mooncal

MoonBit 原生 iCalendar 解析器和 RRULE 日程展开库。

icalendar
ics
rrule
calendar
moonbit
moon add ciqingweiyang/mooncal@0.1.2
Download zip
Version
0.1.2
License
Apache-2.0
Last updated
5 hours ago
Downloads
11
README

#MoonCal

MoonCal 是一个 MoonBit 原生 iCalendar 工具库,用于解析 .ics 日历文本、校验日历数据、展开常见 RRULE 重复事件,并处理 VTODO 待办任务和 VFREEBUSY 空闲忙碌时间段。

#项目状态

黑客松版本:0.1.2

当前版本包含命令行示例、可运行基础示例和 82 个测试用例。本地统计约 4,189 行有效 MoonBit 代码,已经达到活动说明中 4,000 到 10,000 行参考规模的下限区间。代码规模来自事件解析、重复规则展开、任务处理、空闲忙碌时间段处理、验证诊断、导出和测试,不包含无意义占位代码。

#功能范围

  • 支持 iCalendar 折行展开;
  • 支持 VCALENDARVEVENTVTODOVFREEBUSY 结构解析;
  • 支持属性参数,例如 DTSTART;VALUE=DATEFREEBUSY;FBTYPE=BUSY-TENTATIVE
  • 支持事件字段:UIDDTSTARTDTENDDURATIONSUMMARYDESCRIPTIONLOCATIONSTATUSURLCATEGORIESCREATEDLAST-MODIFIEDSEQUENCE
  • 支持重复规则:FREQINTERVALCOUNTUNTILBYDAYBYMONTHDAYBYMONTH
  • 支持 DAILYWEEKLYMONTHLYYEARLY 四类常见重复频率;
  • 支持 RDATE 追加日期和 EXDATE 排除日期;
  • 支持待办任务字段:DUECOMPLETEDSTATUSPRIORITYPERCENT-COMPLETERELATED-TOCATEGORIES
  • 支持任务查询:打开任务、已完成任务、逾期任务、指定日期到期任务、高优先级任务;
  • 支持空闲忙碌时间段解析:BUSYBUSY-TENTATIVEBUSY-UNAVAILABLEFREE
  • 支持忙碌窗口查询、指定时刻忙碌判断、后续忙碌时间段查询;
  • 提供日历摘要、任务摘要、空闲忙碌摘要、JSON 输出和基础 ICS 导出能力。

#非目标范围

  • 不做 CalDAV 网络同步;
  • 不内置完整时区数据库和 VTIMEZONE 解析;
  • 不实现会议邀请回复、参会人状态同步等完整协作流程;
  • 不覆盖 BYSETPOSWKST1MO 这类高级 RRULE 组合;
  • 不面向超大 .ics 文件的流式解析。

#安装

通过 Mooncakes 安装:

moon add ciqingweiyang/mooncal

本地开发和验收命令:

moon check moon build moon test moon run cmd/main moon run cmd/main -- --json moon run examples/basic

#最小示例

match @mooncal.parse(@mooncal.sample_ics) {
Ok(calendar) => {
let from = @mooncal.DateTime(2026, 8, 1)
let until = @mooncal.DateTime(2026, 8, 31)
match @mooncal.occurrences_between(calendar, from, until, limit=32) {
Ok(items) => println(@mooncal.occurrences_to_json(items))
Err(err) => println(@mooncal.error_to_text(err))
}
let due = @mooncal.tasks_due_on_date(calendar, @mooncal.DateTime(2026, 8, 18))
println(@mooncal.task_to_json(due[0]))
}
Err(err) => println(@mooncal.error_to_text(err))
}

#命令行示例

moon run cmd/main moon run cmd/main -- --json

命令行示例会分析内置日历样例,并输出文本摘要或 JSON 摘要。文件输入不属于 0.1.2 范围,以保持核心库在不同 MoonBit 目标上的可移植性。

#接口概览

接口用途
parse(input)解析 VCALENDAR 文档
parse_rule(input)解析单条 RRULE
occurrences_between(calendar, from, until, limit=...)展开指定时间窗口内的事件
occurrences_on_date(calendar, date)展开指定日期的事件
next_occurrences(calendar, after)查询后续即将发生的事件
open_tasks(calendar)查询未完成、未取消的任务
completed_tasks(calendar)查询已完成任务
overdue_tasks(calendar, as_of)按调用方给定日期查询逾期任务
tasks_due_on_date(calendar, date)查询指定日期到期任务
high_priority_tasks(calendar)查询高优先级任务
busy_periods_between(calendar, from, until)查询时间窗口内的忙碌时间段
free_periods_between(calendar, from, until)查询显式空闲时间段
time_is_busy(calendar, moment)判断某个时刻是否忙碌
analyze(input)解析日历并生成摘要
validate_calendar(calendar)返回日历验证诊断
calendar_to_json(calendar)输出解析后的日历 JSON
calendar_to_ics(calendar)导出基础 VCALENDAR 文本
summary_to_text(summary)输出适合命令行展示的文本摘要
error_to_text(err)将类型化错误转换为可读文本

#项目定位

MoonCal 补足 MoonBit 生态中日历数据处理能力的空白。它适用于排期工具、提醒工具、静态站点生成器、测试数据处理器,以及需要稳定解析日历数据的自动化流程。

项目选择用 MoonBit 原生实现,不依赖 JavaScript、C 或其他语言库封装,便于在 MoonBit 生态中直接复用、测试、发布和维护。

#验收材料

验收自查、撞车检查和本地验证记录见 docs/ACCEPTANCE_SELF_CHECK.md

#原创性与参考

MoonCal 是原创 MoonBit 实现。项目参考公开 iCalendar 标准 RFC 5545 的格式和概念说明,但没有复制其他项目代码。测试样例由项目自行构造。

#许可证

Apache-2.0。

#
Calendar

pub(all) struct Calendar {
version : String?
prodid : String?
events : Array[Event]
tasks : Array[Task]
freebusy : Array[FreeBusy]
properties : Array[IcsProperty]
} derive(
Debug
)

A parsed VCALENDAR document.

#
CalendarSummary

pub(all) struct CalendarSummary {
event_count : Int
recurring_event_count : Int
all_day_event_count : Int
task_count : Int
open_task_count : Int
completed_task_count : Int
blocked_time_count : Int
blocked_period_count : Int
earliest : DateTime?
latest : DateTime?
diagnostics : Array[Diagnostic]
} derive(
Debug
)

Human-readable summary for CLI output and README examples.

#
DateTime

pub(all) struct DateTime {
year : Int
month : Int
day : Int
hour : Int
minute : Int
second : Int
is_date : Bool
is_utc : Bool
} derive(Eq,
Debug
)

Calendar date or date-time. is_date=true represents all-day DATE values.
impl Show for DateTime

#
DateTime::DateTime

fn DateTime::DateTime(year : Int, month : Int, day : Int, hour? : Int, minute? : Int, second? : Int, is_date? : Bool, is_utc? : Bool) -> DateTime

#
DateTime::add_days

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

#
DateTime::add_months

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

#
DateTime::add_seconds

fn DateTime::add_seconds(self : DateTime, seconds : Int) -> DateTime

#
DateTime::add_years

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

#
DateTime::after

fn DateTime::after(self : DateTime, other : DateTime) -> Bool

#
DateTime::before

fn DateTime::before(self : DateTime, other : DateTime) -> Bool

#
DateTime::compare

fn DateTime::compare(self : DateTime, other : DateTime) -> Int

#
DateTime::date_key

fn DateTime::date_key(self : DateTime) -> Int

#
DateTime::days_until

fn DateTime::days_until(self : DateTime, other : DateTime) -> Int

#
DateTime::format

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

#
DateTime::is_valid

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

#
DateTime::months_until

fn DateTime::months_until(self : DateTime, other : DateTime) -> Int

#
DateTime::same_day

fn DateTime::same_day(self : DateTime, other : DateTime) -> Bool

#
DateTime::seconds_until

fn DateTime::seconds_until(self : DateTime, other : DateTime) -> Int

#
DateTime::to_ics_value

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

#
DateTime::weekday

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

#
DateTime::years_until

fn DateTime::years_until(self : DateTime, other : DateTime) -> Int

#
Diagnostic

pub(all) struct Diagnostic {
code : String
message : String
severity : String
} derive(Eq,
Debug
)

A validation or analysis note.

#
Duration

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

RFC-style duration subset used by VEVENT DURATION.
impl Show for Duration

#
Duration::Duration

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

#
Duration::apply

fn Duration::apply(self : Duration, start : DateTime) -> DateTime

#
Duration::format

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

#
Duration::is_zero

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

#
Duration::total_seconds

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

#
Event

pub(all) struct Event {
uid : String
start : DateTime
end : DateTime?
summary : String
description : String?
location : String?
status : String?
url : String?
categories : Array[String]
created : DateTime?
last_modified : DateTime?
sequence : Int?
rrule : RRule?
duration : Duration?
rdate : Array[DateTime]
exdate : Array[DateTime]
raw : Array[IcsProperty]
} derive(
Debug
)

A VEVENT record with the fields MoonCal v1 understands.

#
FreeBusy

pub(all) struct FreeBusy {
uid : String?
start : DateTime?
end : DateTime?
dtstamp : DateTime?
organizer : String?
attendees : Array[String]
comments : Array[String]
url : String?
periods : Array[FreeBusyPeriod]
raw : Array[IcsProperty]
} derive(
Debug
)

A VFREEBUSY record used to describe blocked or available time windows.

#
FreeBusyPeriod

pub(all) struct FreeBusyPeriod {
start : DateTime
end : DateTime
busy_type : String
} derive(Eq,
Debug
)

One FREEBUSY period inside a VFREEBUSY component.

#
FreeBusySummary

pub(all) struct FreeBusySummary {
component_count : Int
period_count : Int
busy_period_count : Int
tentative_period_count : Int
unavailable_period_count : Int
free_period_count : Int
earliest_start : DateTime?
latest_end : DateTime?
} derive(
Debug
)

Availability-focused summary over parsed VFREEBUSY data.

#
Frequency

pub(all) enum Frequency {
Daily
Weekly
Monthly
Yearly
} derive(Eq,
Debug
)

Supported recurrence frequencies for v1.

#
IcsProperty

pub(all) struct IcsProperty {
name : String
params : Map[String, String]
value : String
line : Int
} derive(
Debug
)

A parsed iCalendar property after RFC-style line unfolding.

#
MoonCalError

pub(all) enum MoonCalError {
InvalidLine(line~ : Int, message~ : String)
InvalidProperty(line~ : Int, name~ : String, message~ : String)
InvalidDateTime(value~ : String)
InvalidRRule(value~ : String, message~ : String)
InvalidCalendar(message~ : String)
MissingEventField(field~ : String)
MissingTaskField(field~ : String)
UnsupportedFeature(feature~ : String)
LimitExceeded(limit~ : Int)
} derive(Eq,
Debug
)

Errors are data, not panics. This makes CLI output and tests predictable.

#
Occurrence

pub(all) struct Occurrence {
uid : String
start : DateTime
end : DateTime?
summary : String
} derive(Eq,
Debug
)

A concrete occurrence of an event after expanding recurrence data.

#
RRule

pub(all) struct RRule {
freq : Frequency
interval : Int
count : Int?
until : DateTime?
byday : Array[Weekday]
bymonthday : Array[Int]
bymonth : Array[Int]
} derive(Eq,
Debug
)

A v1 recurrence rule. Unsupported RRULE parts are rejected during parsing.

#
RRule::RRule

fn RRule::RRule(freq : Frequency, interval? : Int, count? : Int, until? : DateTime, byday? : Array[Weekday], bymonthday? : Array[Int], bymonth? : Array[Int]) -> RRule

#
Task

pub(all) struct Task {
uid : String
start : DateTime?
due : DateTime?
completed : DateTime?
created : DateTime?
last_modified : DateTime?
summary : String
description : String?
status : String?
priority : Int?
percent_complete : Int?
categories : Array[String]
url : String?
related_to : Array[String]
raw : Array[IcsProperty]
} derive(
Debug
)

A VTODO record with common task-tracking fields.

#
TaskSummary

pub(all) struct TaskSummary {
task_count : Int
open_task_count : Int
completed_task_count : Int
in_process_task_count : Int
cancelled_task_count : Int
high_priority_task_count : Int
due_task_count : Int
earliest_due : DateTime?
latest_due : DateTime?
} derive(
Debug
)

Task-focused summary that callers can compute independently from CLI output.

#
Weekday

pub(all) enum Weekday {
MO
TU
WE
TH
FR
SA
SU
} derive(Eq,
Debug
)

Weekdays use the iCalendar two-letter names.

#
analyze

fn analyze(input : String) -> Result[CalendarSummary, MoonCalError]

Parse a document and immediately return an analysis summary.

#
busy_periods_between

fn busy_periods_between(calendar : Calendar, from : DateTime, until : DateTime) -> Array[FreeBusyPeriod]

#
calendar_to_ics

fn calendar_to_ics(calendar : Calendar) -> String

#
calendar_to_json

fn calendar_to_json(calendar : Calendar) -> String

#
cancelled_tasks

fn cancelled_tasks(calendar : Calendar) -> Array[Task]

#
completed_tasks

fn completed_tasks(calendar : Calendar) -> Array[Task]

#
days_in_month

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

#
error_to_text

fn error_to_text(err : MoonCalError) -> String

#
escape_ics_text

fn escape_ics_text(value : String) -> String

#
escape_json

fn escape_json(value : String) -> String

#
event_effective_end

fn event_effective_end(event : Event) -> DateTime?

#
event_is_recurring

fn event_is_recurring(event : Event) -> Bool

#
event_overlaps

fn event_overlaps(event : Event, from : DateTime, until : DateTime) -> Bool

#
event_to_ics

fn event_to_ics(event : Event) -> String

#
event_to_json

fn event_to_json(event : Event) -> String

#
events_between

fn events_between(calendar : Calendar, from : DateTime, until : DateTime) -> Array[Event]

#
expand_calendar_between

fn expand_calendar_between(calendar : Calendar, from : DateTime, until : DateTime, limit? : Int) -> Result[Array[Occurrence], MoonCalError]

#
expand_event_between

fn expand_event_between(event : Event, from : DateTime, until : DateTime, limit? : Int) -> Result[Array[Occurrence], MoonCalError]

#
find_event

fn find_event(calendar : Calendar, uid : String) -> Event?

#
find_task

fn find_task(calendar : Calendar, uid : String) -> Task?

#
free_periods_between

fn free_periods_between(calendar : Calendar, from : DateTime, until : DateTime) -> Array[FreeBusyPeriod]

#
freebusy_is_busy_type

fn freebusy_is_busy_type(period : FreeBusyPeriod) -> Bool

#
freebusy_period_contains

fn freebusy_period_contains(period : FreeBusyPeriod, moment : DateTime) -> Bool

#
freebusy_period_overlaps

fn freebusy_period_overlaps(period : FreeBusyPeriod, from : DateTime, until : DateTime) -> Bool

#
freebusy_period_to_ics

fn freebusy_period_to_ics(period : FreeBusyPeriod) -> String

#
freebusy_period_to_json

fn freebusy_period_to_json(period : FreeBusyPeriod) -> String

#
freebusy_periods_between

fn freebusy_periods_between(calendar : Calendar, from : DateTime, until : DateTime) -> Array[FreeBusyPeriod]

#
freebusy_summary_to_json

fn freebusy_summary_to_json(summary : FreeBusySummary) -> String

#
freebusy_to_ics

fn freebusy_to_ics(item : FreeBusy) -> String

#
freebusy_to_json

fn freebusy_to_json(item : FreeBusy) -> String

#
frequency_to_string

fn frequency_to_string(freq : Frequency) -> String

#
high_priority_tasks

fn high_priority_tasks(calendar : Calendar) -> Array[Task]

#
is_leap_year

fn is_leap_year(year : Int) -> Bool

#
is_valid_date

fn is_valid_date(year : Int, month : Int, day : Int) -> Bool

#
next_busy_periods

fn next_busy_periods(calendar : Calendar, after : DateTime, limit? : Int) -> Array[FreeBusyPeriod]

#
next_occurrences

fn next_occurrences(calendar : Calendar, after : DateTime, limit? : Int) -> Result[Array[Occurrence], MoonCalError]

#
occurrences_between

fn occurrences_between(calendar : Calendar, from : DateTime, until : DateTime, limit? : Int) -> Result[Array[Occurrence], MoonCalError]

Expand every event in a calendar inside an inclusive time window.

#
occurrences_on_date

fn occurrences_on_date(calendar : Calendar, date : DateTime, limit? : Int) -> Result[Array[Occurrence], MoonCalError]

#
occurrences_to_json

fn occurrences_to_json(items : Array[Occurrence]) -> String

#
open_tasks

fn open_tasks(calendar : Calendar) -> Array[Task]

#
overdue_tasks

fn overdue_tasks(calendar : Calendar, as_of : DateTime) -> Array[Task]

#
package_name

let package_name : String

MoonCal is a small MoonBit-native iCalendar toolkit.

The v1 surface intentionally focuses on reusable library behavior: parsing unfolded ICS properties, extracting VEVENT/VTODO/VFREEBUSY records, expanding common RRULE patterns, validating calendar shape, and exporting summaries.

#
parse

fn parse(input : String) -> Result[Calendar, MoonCalError]

Parse an iCalendar document, returning structured data or a typed error.

#
parse_calendar

fn parse_calendar(input : String) -> Result[Calendar, MoonCalError]

#
parse_datetime

fn parse_datetime(value : String, is_date_hint? : Bool) -> Result[DateTime, MoonCalError]

#
parse_duration

fn parse_duration(input : String) -> Result[Duration, MoonCalError]

#
parse_event

fn parse_event(props : Array[IcsProperty]) -> Result[Event, MoonCalError]

#
parse_freebusy

fn parse_freebusy(props : Array[IcsProperty]) -> Result[FreeBusy, MoonCalError]

#
parse_frequency

fn parse_frequency(value : String) -> Frequency?

#
parse_properties

fn parse_properties(input : String) -> Result[Array[IcsProperty], MoonCalError]

#
parse_property_line

fn parse_property_line(line : String, line_number : Int) -> Result[IcsProperty, MoonCalError]

#
parse_rrule

fn parse_rrule(input : String) -> Result[RRule, MoonCalError]

#
parse_rule

fn parse_rule(input : String) -> Result[RRule, MoonCalError]

Parse a single recurrence rule value, for example FREQ=WEEKLY;COUNT=3;BYDAY=MO,WE.

#
parse_task

fn parse_task(props : Array[IcsProperty]) -> Result[Task, MoonCalError]

#
parse_weekday

fn parse_weekday(value : String) -> Weekday?

#
rrule_to_ics

fn rrule_to_ics(rule : RRule) -> String

#
sample_ics

let sample_ics : String

A compact fixture used by the CLI, examples, and smoke tests.

#
summarize

fn summarize(calendar : Calendar) -> CalendarSummary

#
summarize_freebusy

fn summarize_freebusy(calendar : Calendar) -> FreeBusySummary

#
summarize_tasks

fn summarize_tasks(calendar : Calendar) -> TaskSummary

#
summary_to_json

fn summary_to_json(summary : CalendarSummary) -> String

#
summary_to_text

fn summary_to_text(summary : CalendarSummary) -> String

#
task_due_between

fn task_due_between(task : Task, from : DateTime, until : DateTime) -> Bool

#
task_due_on_date

fn task_due_on_date(task : Task, date : DateTime) -> Bool

#
task_is_cancelled

fn task_is_cancelled(task : Task) -> Bool

#
task_is_completed

fn task_is_completed(task : Task) -> Bool

#
task_is_in_process

fn task_is_in_process(task : Task) -> Bool

#
task_is_open

fn task_is_open(task : Task) -> Bool

#
task_is_overdue

fn task_is_overdue(task : Task, as_of : DateTime) -> Bool

#
task_summary_to_json

fn task_summary_to_json(summary : TaskSummary) -> String

#
task_to_ics

fn task_to_ics(task : Task) -> String

#
task_to_json

fn task_to_json(task : Task) -> String

#
tasks_due_between

fn tasks_due_between(calendar : Calendar, from : DateTime, until : DateTime) -> Array[Task]

#
tasks_due_on_date

fn tasks_due_on_date(calendar : Calendar, date : DateTime) -> Array[Task]

#
time_is_busy

fn time_is_busy(calendar : Calendar, moment : DateTime) -> Bool

#
unescape_text

fn unescape_text(value : String) -> String

#
unfold_lines

fn unfold_lines(input : String) -> Result[Array[(Int, String)], MoonCalError]

#
validate_calendar

fn validate_calendar(calendar : Calendar) -> Array[Diagnostic]

#
validate_event

fn validate_event(event : Event) -> Array[Diagnostic]

#
validate_freebusy

fn validate_freebusy(item : FreeBusy) -> Array[Diagnostic]

#
validate_task

fn validate_task(task : Task) -> Array[Diagnostic]

#
weekday_to_string

fn weekday_to_string(day : Weekday) -> String