///|
pub fn parse_recurrence_rule(
input : String,
) -> Result[RecurrenceRule, RecurrenceError] {
Ok(parse_recurrence_rule_or_raise(input)) catch {
err => Err(err)
}
}
///|
fn parse_recurrence_rule_or_raise(
input : String,
) -> RecurrenceRule raise RecurrenceError {
let raw = trim_ascii(input)
if raw.length() == 0 {
raise EmptyRule
}
let parts = split_by_char(raw, 59)
let seen : Array[String] = []
let by_day : Array[ByDayRule] = []
let by_month : Array[Int] = []
let by_month_day : Array[Int] = []
let by_set_pos : Array[Int] = []
let mut frequency = Daily
let mut has_frequency = false
let mut interval = 1
let mut count : Int? = None
let mut until : String? = None
let mut wkst : Weekday? = None
for part in parts {
let eq = index_of_char(part, 61)
if eq <= 0 {
raise UnknownPart(part)
}
let key = ascii_upper(trim_ascii(part[0:eq].to_owned()))
let value = trim_ascii(part[eq + 1:part.length()].to_owned())
if contains_string(seen, key) {
raise DuplicatePart(key)
}
seen.push(key)
if key == "FREQ" {
frequency = parse_frequency(value)
has_frequency = true
} else if key == "INTERVAL" {
interval = parse_positive_int(value)
} else if key == "COUNT" {
count = Some(parse_positive_int(value))
} else if key == "UNTIL" {
until = Some(parse_until_value(value))
} else if key == "WKST" {
wkst = Some(parse_weekday(value))
} else if key == "BYDAY" {
parse_by_day_list(value, by_day)
} else if key == "BYMONTH" {
parse_int_list(value, by_month, 1, 12, false)
} else if key == "BYMONTHDAY" {
parse_int_list(value, by_month_day, -31, 31, true)
} else if key == "BYSETPOS" {
parse_int_list(value, by_set_pos, -366, 366, true)
} else {
raise UnknownPart(key)
}
}
if !has_frequency {
raise MissingFrequency
}
match (count, until) {
(Some(_), Some(_)) => raise ConflictingLimit
_ => ()
}
{
frequency,
interval,
count,
until,
wkst,
by_day,
by_month,
by_month_day,
by_set_pos,
}
}
///|
pub fn render_recurrence_rule(rule : RecurrenceRule) -> String {
let b = StringBuilder::new()
b.write_string("FREQ=")
b.write_string(rule.frequency.name())
if rule.interval != 1 {
b.write_string(";INTERVAL=")
b.write_string(rule.interval.to_string())
}
match rule.count {
Some(count) => {
b.write_string(";COUNT=")
b.write_string(count.to_string())
}
None => ()
}
match rule.until {
Some(until) => {
b.write_string(";UNTIL=")
b.write_string(until)
}
None => ()
}
match rule.wkst {
Some(day) => {
b.write_string(";WKST=")
b.write_string(day.abbr())
}
None => ()
}
if rule.by_day.length() > 0 {
b.write_string(";BYDAY=")
b.write_string(render_by_day_list(rule.by_day))
}
if rule.by_month.length() > 0 {
b.write_string(";BYMONTH=")
b.write_string(render_int_list(rule.by_month))
}
if rule.by_month_day.length() > 0 {
b.write_string(";BYMONTHDAY=")
b.write_string(render_int_list(rule.by_month_day))
}
if rule.by_set_pos.length() > 0 {
b.write_string(";BYSETPOS=")
b.write_string(render_int_list(rule.by_set_pos))
}
b.to_string()
}
///|
pub fn normalize_recurrence_rule(
input : String,
) -> Result[String, RecurrenceError] {
match parse_recurrence_rule(input) {
Ok(rule) => Ok(render_recurrence_rule(rule))
Err(err) => Err(err)
}
}
///|
pub fn RecurrenceRule::is_finite(self : RecurrenceRule) -> Bool {
self.count is Some(_) || self.until is Some(_)
}
///|
pub fn RecurrenceRule::has_by_rules(self : RecurrenceRule) -> Bool {
self.by_day.length() > 0 ||
self.by_month.length() > 0 ||
self.by_month_day.length() > 0 ||
self.by_set_pos.length() > 0
}
///|
pub fn RecurrenceRule::uses_weekday(
self : RecurrenceRule,
day : Weekday,
) -> Bool {
for item in self.by_day {
if item.weekday == day {
return true
}
}
false
}
///|
pub fn RecurrenceRule::uses_month(self : RecurrenceRule, month : Int) -> Bool {
for item in self.by_month {
if item == month {
return true
}
}
false
}
///|
pub fn RecurrenceRule::uses_month_day(self : RecurrenceRule, day : Int) -> Bool {
for item in self.by_month_day {
if item == day {
return true
}
}
false
}
///|
pub fn RecurrenceRule::limit_description(self : RecurrenceRule) -> String {
match self.count {
Some(count) => "COUNT=\{count}"
None =>
match self.until {
Some(until) => "UNTIL=\{until}"
None => "unbounded"
}
}
}
///|
pub fn RecurrenceRule::frequency_score(self : RecurrenceRule) -> Int {
match self.frequency {
Secondly => 0
Minutely => 1
Hourly => 2
Daily => 3
Weekly => 4
Monthly => 5
Yearly => 6
}
}
///|
pub fn RecurrenceRule::density_hint(self : RecurrenceRule) -> String {
match self.frequency {
Secondly => "very-high"
Minutely => "high"
Hourly => "medium-high"
Daily => "medium"
Weekly => "medium-low"
Monthly => "low"
Yearly => "very-low"
}
}
///|
pub fn RecurrenceRule::shape_label(self : RecurrenceRule) -> String {
let b = StringBuilder::new()
b.write_string(self.frequency.name())
if self.interval != 1 {
b.write_string("/")
b.write_string(self.interval.to_string())
}
if self.by_day.length() > 0 {
b.write_string(" days=")
b.write_string(render_by_day_list(self.by_day))
}
if self.by_month.length() > 0 {
b.write_string(" months=")
b.write_string(render_int_list(self.by_month))
}
if self.by_month_day.length() > 0 {
b.write_string(" monthdays=")
b.write_string(render_int_list(self.by_month_day))
}
b.to_string()
}
///|
pub fn RecurrenceRule::estimated_upper_bound(self : RecurrenceRule) -> Int? {
match self.count {
Some(count) => Some(count)
None =>
match self.until {
Some(_) => Some(10000)
None => None
}
}
}
///|
pub fn RecurrenceRule::looks_dense_for_feed(self : RecurrenceRule) -> Bool {
if self.frequency_score() <= 2 {
return true
}
match self.count {
Some(count) => count > 5000
None => self.until is None && self.frequency_score() <= 3
}
}
///|
pub fn recurrence_rule_from_parts(
frequency : RecurrenceFrequency,
interval : Int,
count : Int?,
until : String?,
) -> RecurrenceRule {
{
frequency,
interval,
count,
until,
wkst: None,
by_day: [],
by_month: [],
by_month_day: [],
by_set_pos: [],
}
}
///|
pub fn daily_rule(count : Int) -> RecurrenceRule {
recurrence_rule_from_parts(Daily, 1, Some(count), None)
}
///|
pub fn weekly_rule(count : Int, days : Array[Weekday]) -> RecurrenceRule {
let by_day : Array[ByDayRule] = []
for day in days {
by_day.push({ ordinal: None, weekday: day })
}
{
frequency: Weekly,
interval: 1,
count: Some(count),
until: None,
wkst: None,
by_day,
by_month: [],
by_month_day: [],
by_set_pos: [],
}
}
///|
pub fn monthly_nth_weekday_rule(
count : Int,
ordinal : Int,
weekday : Weekday,
) -> RecurrenceRule {
{
frequency: Monthly,
interval: 1,
count: Some(count),
until: None,
wkst: None,
by_day: [{ ordinal: Some(ordinal), weekday }],
by_month: [],
by_month_day: [],
by_set_pos: [],
}
}
///|
fn parse_frequency(value : String) -> RecurrenceFrequency raise RecurrenceError {
let normalized = ascii_upper(trim_ascii(value))
if normalized == "SECONDLY" {
Secondly
} else if normalized == "MINUTELY" {
Minutely
} else if normalized == "HOURLY" {
Hourly
} else if normalized == "DAILY" {
Daily
} else if normalized == "WEEKLY" {
Weekly
} else if normalized == "MONTHLY" {
Monthly
} else if normalized == "YEARLY" {
Yearly
} else {
raise BadFrequency(value)
}
}
///|
fn parse_weekday(value : String) -> Weekday raise RecurrenceError {
let normalized = ascii_upper(trim_ascii(value))
if normalized == "MO" {
Monday
} else if normalized == "TU" {
Tuesday
} else if normalized == "WE" {
Wednesday
} else if normalized == "TH" {
Thursday
} else if normalized == "FR" {
Friday
} else if normalized == "SA" {
Saturday
} else if normalized == "SU" {
Sunday
} else {
raise BadWeekday(value)
}
}
///|
fn parse_until_value(value : String) -> String raise RecurrenceError {
let normalized = trim_ascii(value)
if is_date(normalized) || is_datetime(normalized) {
normalized
} else {
raise BadUntil(value)
}
}
///|
fn parse_int_list(
value : String,
out : Array[Int],
min : Int,
max : Int,
reject_zero : Bool,
) -> Unit raise RecurrenceError {
let parts = split_by_char(value, 44)
for raw in parts {
let n = parse_signed_int(raw)
if n < min || n > max || (reject_zero && n == 0) {
raise BadInteger(raw)
}
out.push(n)
}
}
///|
fn parse_by_day_list(
value : String,
out : Array[ByDayRule],
) -> Unit raise RecurrenceError {
let parts = split_by_char(value, 44)
for raw in parts {
out.push(parse_by_day(raw))
}
}
///|
fn parse_by_day(raw : String) -> ByDayRule raise RecurrenceError {
let value = ascii_upper(trim_ascii(raw))
if value.length() < 2 {
raise BadByDay(raw)
}
let day_text = value[value.length() - 2:value.length()].to_owned()
let weekday = parse_weekday(day_text)
let prefix = value[0:value.length() - 2].to_owned()
if prefix.length() == 0 {
{ ordinal: None, weekday }
} else {
let ordinal = parse_signed_int(prefix)
if ordinal == 0 || ordinal < -53 || ordinal > 53 {
raise BadByDay(raw)
}
{ ordinal: Some(ordinal), weekday }
}
}
///|
fn parse_positive_int(raw : String) -> Int raise RecurrenceError {
let n = parse_signed_int(raw)
if n <= 0 {
raise BadInteger(raw)
}
n
}
///|
fn parse_signed_int(raw : String) -> Int raise RecurrenceError {
let value = trim_ascii(raw)
if value.length() == 0 {
raise BadInteger(raw)
}
let mut sign = 1
let mut i = 0
if value[0] == 45 {
sign = -1
i = 1
} else if value[0] == 43 {
i = 1
}
if i >= value.length() {
raise BadInteger(raw)
}
let mut total = 0
while i < value.length() {
let code = value[i].to_int()
if code < 48 || code > 57 {
raise BadInteger(raw)
}
total = total * 10 + code - 48
i += 1
}
total * sign
}
///|
fn render_by_day_list(days : Array[ByDayRule]) -> String {
let b = StringBuilder::new()
let mut i = 0
while i < days.length() {
if i > 0 {
b.write_string(",")
}
match days[i].ordinal {
Some(ordinal) => b.write_string(ordinal.to_string())
None => ()
}
b.write_string(days[i].weekday.abbr())
i += 1
}
b.to_string()
}
///|
fn render_int_list(values : Array[Int]) -> String {
let b = StringBuilder::new()
let mut i = 0
while i < values.length() {
if i > 0 {
b.write_string(",")
}
b.write_string(values[i].to_string())
i += 1
}
b.to_string()
}
///|
fn contains_string(values : Array[String], target : String) -> Bool {
for value in values {
if value == target {
return true
}
}
false
}