rrule_lab

An explainable RFC 5545 recurrence rule engine written in MoonBit

calendar
rrule
rfc5545
scheduler
moon add cjkzD/rrule_lab@0.1.0
Download zip
Author
Version
0.1.0
License
Apache-2.0
Last updated
4 days ago
Downloads
2
README

#RRuleLab

CI License

RRuleLab 是一个使用 MoonBit 原创实现的、可解释且可审计的 RFC 5545 重复规则引擎。它把 RRULERDATEEXDATE 转换为确定的时间序列,并进一步回答“规则为什么这样运行”“修改规则会影响哪些日期”“多个日程是否冲突”等问题。

项目面向日历、预约、排班、提醒、维护窗口和自动化任务。它不是 MoonBit 编译器周边工具,也不是普通 cron 解析器;核心差异在于 RFC 5545 语义、行为级差异分析和可解释日程审计。

#功能

  • 解析、校验、规范化并序列化 RFC 5545 RRULE
  • 支持 SECONDLYYEARLY、主要 BY* 过滤器、负序号及 BYSETPOS
  • 在有界窗口内安全展开规则,带结果与迭代上限
  • 合并 DTSTART、多个 RRULERDATEEXDATE
  • 读取与生成 recurrence 相关的 iCalendar/VEVENT 子集
  • 中英文规则解释、字段级诊断与修复建议
  • 比较两个规则的新增、删除、共有实例及首次分歧
  • 审计重复实例、周末、非工作时段、过密计划和间隔分布
  • 检测带持续时间事件的区间冲突,计算忙碌窗口与空闲窗口
  • ISO 8601 持续时间和节假日感知的业务日历调整
  • 可作为 MoonBit 库使用,也提供可运行 CLI

#五分钟开始

需要 MoonBit 工具链。在仓库根目录执行:

moon check moon test moon run cmd/rrule_lab -- demo

规范化并解释规则:

moon run cmd/rrule_lab -- normalize "RRULE:FREQ=WEEKLY;BYDAY=WE,MO,MO" moon run cmd/rrule_lab -- explain "FREQ=MONTHLY;BYDAY=-1FR" zh

展开一个真实日程:

moon run cmd/rrule_lab -- expand \ 20260801T090000 \ "FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR" \ 20260801T000000 \ 20260901T000000 \ 20

审计日程或比较规则变更:

moon 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 20260820T000000

完整命令列表:

moon run cmd/rrule_lab -- help

#库 API 示例

let 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())

在其他 MoonBit 项目中使用发布版本:

moon add cjkzD/rrule_lab

#工程质量

  • 主要实现语言:MoonBit
  • 有效 MoonBit 源码:4053 行(包含测试,不包含 _build
  • 自动化测试:51 项,覆盖 Wasm 与 JavaScript 目标
  • CI:检查、测试、构建、格式、CLI 示例、代码规模门禁
  • 许可证:Apache-2.0;运行时仅依赖 MoonBit 官方 core
  • 开发记录:真实功能拆分提交,保留架构、变更与发布说明

#功能边界

首个版本专注本地日期时间与 recurrence 语义,不包含 CalDAV 同步、完整 IANA 时区数据库、自然语言模型解析或完整 iCalendar 全组件支持。浮动本地时间与时区换算明确分层,避免在规则运算中隐式引入环境差异。

#许可证

Copyright 2026 cjkzD。根据 Apache License 2.0 开源。

#
BusinessCalendarError

pub suberror BusinessCalendarError {
EmptyWorkingWeek
SearchLimitExceeded(Date, Int)
} derive(Eq,
Debug
)

#
CliError

pub suberror CliError {
MissingArgument(String)
InvalidNumber(String, String)
UnknownCommand(String)
InvalidLanguage(String)
} derive(Eq,
Debug
)

#
DateTimeError

pub suberror DateTimeError {
InvalidDate(Int, Int, Int)
InvalidTime(Int, Int, Int)
InvalidFormat(String)
} derive(Eq,
Debug
)

#
DurationError

pub suberror DurationError {
EmptyDuration
MissingDesignator
MissingNumber(Int)
InvalidCharacter(Int, Int)
DuplicateUnit(String)
UnitOutOfOrder(String)
DateUnitInTimePart(String)
TimeUnitInDatePart(String)
FractionUnsupported
NonFixedCalendarUnit
} derive(Eq,
Debug
)

#
EventSlotError

pub suberror EventSlotError {
NonPositiveDuration
} derive(Eq,
Debug
)

#
ExpansionError

pub suberror ExpansionError {
InvalidRule(Array[Diagnostic])
InvalidWindow
InvalidLimit(Int)
IterationLimitExceeded(Int)
} derive(
Debug
)

#
ICalendarError

pub suberror ICalendarError {
MissingStart
InvalidContentLine(String)
UnsupportedProperty(String)
InvalidDateValue(String)
} derive(Eq,
Debug
)

#
RRuleParseError

pub suberror RRuleParseError {
EmptyRule
InvalidPart(String)
DuplicateField(String)
MissingFrequency
UnknownFrequency(String)
InvalidInteger(String, String)
InvalidWeekday(String)
InvalidUntil(String)
InvalidValue(String, String)
} derive(Eq,
Debug
)

#
TimeRangeError

pub suberror TimeRangeError {
EmptyOrReversedRange(DateTime, DateTime)
} derive(Eq,
Debug
)

#
AuditFinding

pub struct AuditFinding {
severity : AuditSeverity
code : String
message : String
evidence : Array[DateTime]
suggestion : String
} derive(Eq,
Debug
)

A stable finding code plus human-readable evidence and remediation.

#
AuditSeverity

pub(all) enum AuditSeverity {
AuditInfo
AuditWarning
AuditError
} derive(Compare, Eq,
Debug
)

Severity used by schedule audit findings.

#
AuditSeverity::label

fn AuditSeverity::label(self : AuditSeverity) -> String

#
BusinessCalendar

pub struct BusinessCalendar {
name : String
working_weekdays : Array[Weekday]
holidays : Array[Date]
} derive(Eq,
Debug
)

Named workweek and holiday set. Dates are local calendar dates; no timezone assumptions are introduced by business-day adjustment.

#
BusinessCalendar::add_business_days

fn BusinessCalendar::add_business_days(self : BusinessCalendar, date : Date, amount : Int) -> Date raise BusinessCalendarError

#
BusinessCalendar::add_holiday

fn BusinessCalendar::add_holiday(self : BusinessCalendar, date : Date) -> BusinessCalendar

#
BusinessCalendar::adjust

fn BusinessCalendar::adjust(self : BusinessCalendar, date : Date, convention : BusinessDayConvention) -> Date raise BusinessCalendarError

#
BusinessCalendar::adjust_datetime

#
BusinessCalendar::adjust_occurrences

fn BusinessCalendar::adjust_occurrences(self : BusinessCalendar, values : Array[DateTime], convention : BusinessDayConvention, deduplicate? : Bool) -> Array[BusinessDayAdjustment] raise BusinessCalendarError

#
BusinessCalendar::count_business_days

fn BusinessCalendar::count_business_days(self : BusinessCalendar, start : Date, finish : Date) -> Int

Count working dates in the half-open interval [start, finish).

#
BusinessCalendar::is_holiday

fn BusinessCalendar::is_holiday(self : BusinessCalendar, date : Date) -> Bool

#
BusinessCalendar::is_working_day

fn BusinessCalendar::is_working_day(self : BusinessCalendar, date : Date) -> Bool

#
BusinessCalendar::new

fn BusinessCalendar::new(name : String, working_weekdays? : Array[Weekday], holidays? : Array[Date]) -> BusinessCalendar raise BusinessCalendarError

#
BusinessCalendar::next_working_date

fn BusinessCalendar::next_working_date(self : BusinessCalendar, date : Date, include_current? : Bool, search_limit? : Int) -> Date raise BusinessCalendarError

#
BusinessCalendar::previous_working_date

fn BusinessCalendar::previous_working_date(self : BusinessCalendar, date : Date, include_current? : Bool, search_limit? : Int) -> Date raise BusinessCalendarError

#
BusinessCalendar::remove_holiday

fn BusinessCalendar::remove_holiday(self : BusinessCalendar, date : Date) -> BusinessCalendar

#
BusinessCalendar::to_text

fn BusinessCalendar::to_text(self : BusinessCalendar) -> String

Describe the calendar in a stable, diff-friendly text format.

#
BusinessCalendar::with_working_weekdays

fn BusinessCalendar::with_working_weekdays(self : BusinessCalendar, working_weekdays : Array[Weekday]) -> BusinessCalendar raise BusinessCalendarError

#
BusinessCalendar::working_dates_between

fn BusinessCalendar::working_dates_between(self : BusinessCalendar, start : Date, finish : Date) -> Array[Date]

#
BusinessDayAdjustment

pub struct BusinessDayAdjustment {
original : DateTime
adjusted : DateTime
changed : Bool
convention : BusinessDayConvention
} derive(Eq,
Debug
)

#
BusinessDayConvention

pub(all) enum BusinessDayConvention {
Unadjusted
Following
ModifiedFollowing
Preceding
ModifiedPreceding
} derive(Compare, Eq,
Debug
)

Convention used when an occurrence lands on a non-working date.

#
BusinessDayConvention::label

fn BusinessDayConvention::label(self : BusinessDayConvention) -> String

#
ByDay

pub struct ByDay {
ordinal : Int?
weekday : Weekday
} derive(Compare, Eq,
Debug
)

A weekday optionally qualified with an ordinal such as 1MO or -1FR.

#
ByDay::new

fn ByDay::new(weekday : Weekday, ordinal? : Int) -> ByDay

#
ByDay::to_string

fn ByDay::to_string(self : ByDay) -> String

#
ChangeImpact

pub struct ChangeImpact {
baseline_count : Int
candidate_count : Int
added : Array[DateTime]
removed : Array[DateTime]
unchanged : Array[DateTime]
first_divergence : DateTime?
similarity_permille : Int
} derive(Eq,
Debug
)

Behavioral impact of replacing one rule with another over a finite horizon.

#
CliResult

pub struct CliResult {
exit_code : Int
stdout : String
stderr : String
} derive(Eq,
Debug
)

#
Date

pub struct Date {
year : Int
month : Int
day : Int
} derive(Compare, Eq,
Debug
)

A proleptic Gregorian calendar date.

#
Date::add_days

fn Date::add_days(self : Date, amount : Int) -> Date

#
Date::add_months

fn Date::add_months(self : Date, amount : Int) -> Date

#
Date::add_years

fn Date::add_years(self : Date, amount : Int) -> Date

#
Date::day_of_year

fn Date::day_of_year(self : Date) -> Int

#
Date::new

fn Date::new(year : Int, month : Int, day : Int) -> Date raise DateTimeError

#
Date::to_epoch_day

fn Date::to_epoch_day(self : Date) -> Int

Howard Hinnant's civil calendar conversion, shifted to Unix epoch days.

#
Date::to_iso_string

fn Date::to_iso_string(self : Date) -> String

#
Date::weekday

fn Date::weekday(self : Date) -> Weekday

#
DateTime

pub struct DateTime {
date : Date
time : Time
} derive(Compare, Eq,
Debug
)

A local date and time. RRuleLab deliberately keeps local time separate from time-zone conversion so recurrence arithmetic remains deterministic.

#
DateTime::add_days

fn DateTime::add_days(self : DateTime, amount : Int) -> DateTime

#
DateTime::add_duration

fn DateTime::add_duration(self : DateTime, duration : Duration) -> DateTime

#
DateTime::add_months

fn DateTime::add_months(self : DateTime, amount : Int) -> DateTime

#
DateTime::add_seconds

fn DateTime::add_seconds(self : DateTime, amount : Int64) -> DateTime

#
DateTime::add_years

fn DateTime::add_years(self : DateTime, amount : Int) -> DateTime

#
DateTime::new

fn DateTime::new(year : Int, month : Int, day : Int, hour : Int, minute : Int, second : Int) -> DateTime raise DateTimeError

#
DateTime::to_epoch_second

fn DateTime::to_epoch_second(self : DateTime) -> Int64

#
DateTime::to_iso_string

fn DateTime::to_iso_string(self : DateTime) -> String

#
Diagnostic

pub struct Diagnostic {
severity : Severity
code : String
field : String
message : String
suggestion : String?
} derive(Eq,
Debug
)

Machine-readable and human-friendly diagnostic returned by validation.

#
Duration

pub struct Duration {
years : Int
months : Int
weeks : Int
days : Int
hours : Int
minutes : Int
seconds : Int
negative : Bool
} derive(Eq,
Debug
)

A calendar-aware ISO 8601 duration. Years and months are retained as calendar units; weeks, days and clock fields can also be reduced to seconds.

#
Duration::explain_en

fn Duration::explain_en(self : Duration) -> String

Produce a compact English explanation suitable for reports and CLI output.

#
Duration::explain_zh

fn Duration::explain_zh(self : Duration) -> String

#
Duration::fixed_seconds

fn Duration::fixed_seconds(self : Duration) -> Int64 raise DurationError

#
Duration::has_calendar_units

fn Duration::has_calendar_units(self : Duration) -> Bool

#
Duration::is_zero

fn Duration::is_zero(self : Duration) -> Bool

#
Duration::new

fn Duration::new(years? : Int, months? : Int, weeks? : Int, days? : Int, hours? : Int, minutes? : Int, seconds? : Int, negative? : Bool) -> Duration

#
Duration::sign

fn Duration::sign(self : Duration) -> Int

#
Duration::to_iso_string

fn Duration::to_iso_string(self : Duration) -> String

#
Duration::zero

fn Duration::zero() -> Duration

#
EventSlot

pub struct EventSlot {
schedule : String
ordinal : Int
start : DateTime
finish : DateTime
} derive(Eq,
Debug
)

One occurrence represented as a half-open [start, finish) interval.

#
ExpansionOptions

pub struct ExpansionOptions {
window_start : DateTime
window_end : DateTime
limit : Int
iteration_limit : Int
} derive(Eq,
Debug
)

Bounds and safety limits for a recurrence expansion request.

#
ExpansionOptions::new

fn ExpansionOptions::new(window_start : DateTime, window_end : DateTime, limit? : Int, iteration_limit? : Int) -> ExpansionOptions

#
Frequency

pub enum Frequency {
Secondly
Minutely
Hourly
Daily
Weekly
Monthly
Yearly
} derive(Compare, Eq,
Debug
)

Frequency ordered from the smallest to the largest recurrence period.

#
Frequency::code

fn Frequency::code(self : Frequency) -> String

#
RRule

pub 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
)

A fully typed subset of RFC 5545 RECUR. Empty arrays mean that the corresponding BY rule is absent and DTSTART supplies the default.

#
RRule::explain_en

fn RRule::explain_en(self : RRule) -> String

#
RRule::explain_zh

fn RRule::explain_zh(self : RRule) -> String

#
RRule::is_valid

fn RRule::is_valid(self : RRule) -> Bool

#
RRule::new

fn RRule::new(frequency : Frequency) -> RRule

#
RRule::normalize

fn RRule::normalize(self : RRule) -> RRule

#
RRule::to_string

fn RRule::to_string(self : RRule) -> String

#
RRule::validate

fn RRule::validate(self : RRule) -> Array[Diagnostic]

#
RecurrenceSet

pub struct RecurrenceSet {
start : DateTime
rules : Array[RRule]
include_dates : Array[DateTime]
exclude_dates : Array[DateTime]
} derive(Eq,
Debug
)

A calendar recurrence set combines one or more rules and explicit dates, then removes exception dates. This mirrors RFC 5545 recurrence semantics.

#
RecurrenceSet::add_date

fn RecurrenceSet::add_date(self : RecurrenceSet, value : DateTime) -> RecurrenceSet

#
RecurrenceSet::add_rule

fn RecurrenceSet::add_rule(self : RecurrenceSet, rule : RRule) -> RecurrenceSet

#
RecurrenceSet::exclude_date

fn RecurrenceSet::exclude_date(self : RecurrenceSet, value : DateTime) -> RecurrenceSet

#
RecurrenceSet::expand

#
RecurrenceSet::new

#
RecurrenceSet::to_ical

fn RecurrenceSet::to_ical(self : RecurrenceSet, uid? : String) -> String

#
RuleDifference

pub struct RuleDifference {
only_left : Array[DateTime]
only_right : Array[DateTime]
shared : Array[DateTime]
} derive(Eq,
Debug
)

#
ScheduleAudit

pub 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
)

Summary metrics and findings produced for one expanded recurrence set.

#
ScheduleAudit::to_text

fn ScheduleAudit::to_text(self : ScheduleAudit) -> String

Stable plain-text report intended for terminals, issues and CI logs.

#
ScheduleCollision

pub struct ScheduleCollision {
left : EventSlot
right : EventSlot
overlap_start : DateTime
overlap_finish : DateTime
overlap_seconds : Int64
} derive(Eq,
Debug
)

#
SetOperation

pub(all) enum SetOperation {
Union
Intersection
Difference
} derive(Eq,
Debug
)

#
Severity

pub(all) enum Severity {
Error
Warning
Information
} derive(Compare, Eq,
Debug
)

#
Time

pub struct Time {
hour : Int
minute : Int
second : Int
} derive(Compare, Eq,
Debug
)

Clock time without a time-zone offset.

#
Time::new

fn Time::new(hour : Int, minute : Int, second : Int) -> Time raise DateTimeError

#
Time::to_iso_string

fn Time::to_iso_string(self : Time) -> String

#
TimeRange

pub struct TimeRange {
start : DateTime
finish : DateTime
} derive(Eq,
Debug
)

Half-open time range [start, finish). This representation makes adjacent windows composable without double-counting their shared endpoint.

#
TimeRange::contains

fn TimeRange::contains(self : TimeRange, value : DateTime) -> Bool

#
TimeRange::duration_seconds

fn TimeRange::duration_seconds(self : TimeRange) -> Int64

#
TimeRange::intersection

fn TimeRange::intersection(self : TimeRange, other : TimeRange) -> TimeRange?

#
TimeRange::is_adjacent

fn TimeRange::is_adjacent(self : TimeRange, other : TimeRange) -> Bool

#
TimeRange::merge

fn TimeRange::merge(self : TimeRange, other : TimeRange) -> TimeRange?

#
TimeRange::new

fn TimeRange::new(start : DateTime, finish : DateTime) -> TimeRange raise TimeRangeError

#
TimeRange::overlaps

fn TimeRange::overlaps(self : TimeRange, other : TimeRange) -> Bool

#
TimeRange::to_text

fn TimeRange::to_text(self : TimeRange) -> String

#
Weekday

pub enum Weekday {
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
} derive(Compare, Eq,
Debug
)

Days of the week use ISO order (Monday first).

#
Weekday::code

fn Weekday::code(self : Weekday) -> String

#
Weekday::iso_number

fn Weekday::iso_number(self : Weekday) -> Int

#
WeekdayHistogram

pub struct WeekdayHistogram {
monday : Int
tuesday : Int
wednesday : Int
thursday : Int
friday : Int
saturday : Int
sunday : Int
} derive(Eq,
Debug
)

Distribution of recurrence instances over ISO weekdays.

#
WeekdayHistogram::empty

#
WeekdayHistogram::total

fn WeekdayHistogram::total(self : WeekdayHistogram) -> Int

#
WeekdayHistogram::weekend_total

fn WeekdayHistogram::weekend_total(self : WeekdayHistogram) -> Int

#
analyze_rule_change

fn analyze_rule_change(start : DateTime, baseline : RRule, candidate : RRule, options : ExpansionOptions) -> ChangeImpact raise ExpansionError

Compare rule behavior using occurrence sets rather than textual syntax.

#
audit_occurrences

fn audit_occurrences(values : Array[DateTime], business_start_hour? : Int, business_end_hour? : Int, expected_minimum? : Int, dense_gap_threshold_seconds? : Int64) -> ScheduleAudit

Analyze concrete occurrence data and return reproducible findings.

#
audit_rule

fn audit_rule(start : DateTime, rule : RRule, options : ExpansionOptions, business_start_hour? : Int, business_end_hour? : Int) -> ScheduleAudit raise ExpansionError

Expand and audit a recurrence rule in one bounded operation.

#
business_day_convention_from_string

fn business_day_convention_from_string(text : String) -> BusinessDayConvention?

#
cli_help

fn cli_help() -> String

#
combine_occurrences

fn combine_occurrences(left : Array[DateTime], right : Array[DateTime], operation : SetOperation) -> Array[DateTime]

#
compare_rules

fn compare_rules(start : DateTime, left : RRule, right : RRule, options : ExpansionOptions) -> RuleDifference raise ExpansionError

#
date_from_epoch_day

fn date_from_epoch_day(epoch_day : Int) -> Date

#
days_in_month

fn days_in_month(year : Int, month : Int) -> Int

#
detect_collisions

fn detect_collisions(left : Array[EventSlot], right : Array[EventSlot]) -> Array[ScheduleCollision]

Detect interval collisions between two named schedules. Touching endpoints are not collisions because event slots use half-open interval semantics.

#
duration_between

fn duration_between(start : DateTime, finish : DateTime) -> Duration

#
execute_cli

fn execute_cli(args : Array[String]) -> CliResult

Execute one CLI command without performing process I/O. This design makes the command interface fully testable and portable across Wasm and JS.

#
expand

fn expand(start : DateTime, rule_source : String, window_start : DateTime, window_end : DateTime, limit? : Int) -> Array[DateTime] raise

#
expand_rrule

fn expand_rrule(start : DateTime, rule : RRule, options : ExpansionOptions) -> Array[DateTime] raise ExpansionError

Expand a recurrence within a half-open time window [start, end).

#
free_time_ranges

fn free_time_ranges(bounds : TimeRange, busy : Array[TimeRange]) -> Array[TimeRange]

Find free half-open windows inside bounds after normalizing busy ranges.

#
frequency_from_code

fn frequency_from_code(code : String) -> Frequency?

#
is_leap_year

fn is_leap_year(year : Int) -> Bool

#
make_event_slots

fn make_event_slots(schedule : String, starts : Array[DateTime], duration : Duration) -> Array[EventSlot] raise

#
merge_time_ranges

fn merge_time_ranges(values : Array[TimeRange], merge_adjacent? : Bool) -> Array[TimeRange]

Merge overlapping ranges and, by default, directly adjacent ranges.

#
parse_basic_date

fn parse_basic_date(text : String) -> Date raise DateTimeError

#
parse_date

fn parse_date(text : String) -> Date raise DateTimeError

#
parse_datetime

fn parse_datetime(text : String) -> DateTime raise DateTimeError

#
parse_duration

fn parse_duration(source : String) -> Duration raise DurationError

Parse the integer subset of ISO 8601 durations, including leading minus. Supported examples include P2W, P1Y2M3DT4H5M6S and -PT90S.

#
parse_ical_recurrence

fn parse_ical_recurrence(source : String) -> RecurrenceSet raise

Parse the recurrence-related subset of an iCalendar component.

#
parse_rrule

fn parse_rrule(source : String) -> RRule raise RRuleParseError

Parse an RFC 5545 recurrence rule. The optional RRULE: prefix is accepted.

#
parse_time

fn parse_time(text : String) -> Time raise DateTimeError

#
ranges_total_seconds

fn ranges_total_seconds(values : Array[TimeRange]) -> Int64

#
version

fn version() -> String

Return the semantic version of the RRuleLab library.

#
weekday_from_code

fn weekday_from_code(code : String) -> Weekday?