///|
pub suberror ICalendarError {
MissingStart
InvalidContentLine(String)
UnsupportedProperty(String)
InvalidDateValue(String)
} derive(Eq, Debug)
///|
fn unfold_ical_lines(source : String) -> Array[String] {
let normalized = source.replace_all(old="\r\n", new="\n")
let result : Array[String] = []
for line_view in normalized.split("\n") {
let line = line_view.to_owned()
if line.length() == 0 {
continue
}
if (line.has_prefix(" ") || line.has_prefix("\t")) && result.length() > 0 {
let previous = result[result.length() - 1]
result[result.length() - 1] = previous + line[1:].to_owned()
} else {
result.push(line)
}
}
result
}
///|
fn property_name(left : String) -> String {
let parts : Array[String] = left
.split(";")
.map(value => value.to_owned())
.collect()
parts[0].to_upper()
}
///|
fn parse_datetime_list(value : String) -> Array[DateTime] raise ICalendarError {
let result : Array[DateTime] = []
for item in value.split(",") {
let text = item.to_owned()
let without_z = if text.has_suffix("Z") {
text[:text.length() - 1].to_owned()
} else {
text
}
let parsed = parse_datetime(without_z) catch {
_ => raise InvalidDateValue(text)
}
result.push(parsed)
}
result
}
///|
/// Parse the recurrence-related subset of an iCalendar component.
pub fn parse_ical_recurrence(source : String) -> RecurrenceSet raise {
let mut start : DateTime? = None
let rules : Array[RRule] = []
let included : Array[DateTime] = []
let excluded : Array[DateTime] = []
for line in unfold_ical_lines(source) {
if line.has_prefix("BEGIN:") ||
line.has_prefix("END:") ||
line.has_prefix("VERSION:") ||
line.has_prefix("PRODID:") ||
line.has_prefix("CALSCALE:") ||
line.has_prefix("METHOD:") {
continue
}
let split = line.split_once(":")
let (left_view, value_view) = match split {
Some(pair) => pair
None => raise InvalidContentLine(line)
}
let left = left_view.to_owned()
let value = value_view.to_owned()
match property_name(left) {
"DTSTART" => {
let values = parse_datetime_list(value)
if values.length() != 1 {
raise InvalidDateValue(value)
}
start = Some(values[0])
}
"RRULE" => rules.push(parse_rrule(value))
"RDATE" =>
for item in parse_datetime_list(value) {
included.push(item)
}
"EXDATE" =>
for item in parse_datetime_list(value) {
excluded.push(item)
}
"UID" | "SUMMARY" | "DESCRIPTION" | "DTSTAMP" | "SEQUENCE" | "STATUS" =>
()
name => raise UnsupportedProperty(name)
}
}
let actual_start = match start {
Some(value) => value
None => raise MissingStart
}
{
start: actual_start,
rules,
include_dates: included,
exclude_dates: excluded,
}
}
///|
fn basic_datetime(value : DateTime) -> String {
pad4(value.date.year) +
pad2(value.date.month) +
pad2(value.date.day) +
"T" +
pad2(value.time.hour) +
pad2(value.time.minute) +
pad2(value.time.second)
}
///|
fn fold_content_line(line : String) -> Array[String] {
if line.length() <= 75 {
return [line]
}
let result : Array[String] = []
let mut offset = 0
let mut first = true
while offset < line.length() {
let width = if first { 75 } else { 74 }
let end = (offset + width).min(line.length())
let part = line[offset:end].to_owned()
result.push(if first { part } else { " " + part })
first = false
offset = end
}
result
}
///|
pub fn RecurrenceSet::to_ical(
self : RecurrenceSet,
uid? : String = "rrule-lab@example",
) -> String {
let logical : Array[String] = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//RRuleLab//MoonBit RFC5545 Engine//EN",
"BEGIN:VEVENT",
"UID:" + uid,
"DTSTART:" + basic_datetime(self.start),
]
for rule in self.rules {
logical.push("RRULE:" + rule.to_string())
}
if self.include_dates.length() > 0 {
logical.push(
"RDATE:" +
self.include_dates.map(value => basic_datetime(value)).join(","),
)
}
if self.exclude_dates.length() > 0 {
logical.push(
"EXDATE:" +
self.exclude_dates.map(value => basic_datetime(value)).join(","),
)
}
logical.push("END:VEVENT")
logical.push("END:VCALENDAR")
let physical : Array[String] = []
for line in logical {
for folded in fold_content_line(line) {
physical.push(folded)
}
}
physical.join("\r\n") + "\r\n"
}