Class: Rem2ics::Recurrence

Inherits:
Object
  • Object
show all
Defined in:
lib/rem2ics/recurrence.rb

Overview

How a reminder repeats, expressed the way iCalendar expresses it -- when iCalendar can express it at all.

A reminder's trigger and an RRULE overlap, but neither contains the other. REM 15 and FREQ=MONTHLY;BYMONTHDAY=15 are the same thing. REM Mon 13 SKIP OMIT Sat Sun is not any RRULE: it means "the Monday of the week the 13th falls in, unless that lands on an omitted day, in which case do not trigger at all", and BYDAY has no way to say the second half.

So the rule is a guess, and the guess is checked. Remind::Reminder can produce the dates the reminder actually fires on -- they come from ComputeTrigger, which is what remind itself runs -- so the candidate RRULE is expanded with ice_cube and compared against them. If they agree for the whole horizon, the event carries the rule, and every calendar that reads it recurs forever, correctly. If they disagree anywhere, the event carries Remind's dates instead: fewer of them, but none of them wrong.

That check is the reason this converter is built on bindings rather than on a parser of its own. Without Remind there is nothing to check against, and every previous converter has had to guess and hope.

Defined Under Namespace

Classes: Dates, Rule

Constant Summary collapse

DEFAULT_HORIZON =

How many occurrences the guess has to get right. Ten years of a monthly reminder, or two of a weekly one -- far enough out for the leap years, the month lengths and the moving holidays that break a rule to have shown up.

120

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(reminder, horizon: DEFAULT_HORIZON) ⇒ Recurrence

Returns a new instance of Recurrence.



53
54
55
56
# File 'lib/rem2ics/recurrence.rb', line 53

def initialize(reminder, horizon: DEFAULT_HORIZON)
  @reminder = reminder
  @horizon = horizon
end

Instance Attribute Details

#horizonObject (readonly)

Returns the value of attribute horizon.



51
52
53
# File 'lib/rem2ics/recurrence.rb', line 51

def horizon
  @horizon
end

#reminderObject (readonly)

Returns the value of attribute reminder.



51
52
53
# File 'lib/rem2ics/recurrence.rb', line 51

def reminder
  @reminder
end

Instance Method Details

#callObject



58
59
60
61
62
63
64
65
66
# File 'lib/rem2ics/recurrence.rb', line 58

def call
  if single?
    Dates.new(remind_dates)
  elsif agrees?
    Rule.new(candidate, remind_dates)
  else
    Dates.new(remind_dates)
  end
end

#candidateObject

The RRULE this reminder looks like it means, before anyone checks.

The shape is decided by which parts of the date the trigger left out, because that is how Remind's trigger language works: REM 15 says nothing about the month, so it happens every month.



86
87
88
# File 'lib/rem2ics/recurrence.rb', line 86

def candidate
  @candidate ||= bounded(frequency)
end

#remind_datesObject

The dates Remind says the reminder fires on, bounded by the horizon.



77
78
79
# File 'lib/rem2ics/recurrence.rb', line 77

def remind_dates
  @remind_dates ||= reminder.occurrences(limit: horizon).to_a
end

#single?Boolean

A reminder Remind only ever triggers once needs no rule to describe it, whatever its trigger looks like. Asking Remind is more reliable than reading the trigger: REM 25 Dec 2027 and REM 1 Jan 2027 *14 UNTIL 14 Jan 2027 both happen once, and only one of them looks like it.

Returns:

  • (Boolean)


72
73
74
# File 'lib/rem2ics/recurrence.rb', line 72

def single?
  remind_dates.length <= 1
end