Quoteing hal9000@hypermetrics.com, on Thu, Mar 18, 2004 at 03:40:06PM +0900:
I want to allow recurring tasks. Some of these will be simple, like
“Every Monday.” Others will be more complex, like “Every 2nd and 4th
Friday.” Some might not even be based on weeks or months at all, but
might be like: “Every ten days, no matter what.”
There’d also be an option to give advance warning (N days) on each
event.
So the question becomes: Given a date (typically “today”) and a list
of recurring tasks, how do I determine which ones need to be displayed?
Funny, I’m working on this right now!
I’m adding iCalendar (RFC 2445) support to my vCard library (which is
being renamed vPim).
iCalendar allows events, todos, journal entries, etc. to be stored, and
it also has a very rich language for specifying recurrence rules for
events.
The model is this:
the first occurence it at T
from T you generate new occurences TN for N = 0, … at a fequency of n
(years/months/weeks/days/…)
within that Tn you apply criteria: only on 100th day of year, only on 2
and 3rd sunday of a month, only on tuesdays, …
The criteria results in more or less times.
This might not make sense… here’s some examples:
Weekly on Tuesday and Thursday for 5 weeks:
DTSTART;TZID=US-Eastern:19970902T090000
RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH
or
RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH
==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2
Monthly on the 1st Friday for ten occurrences:
DTSTART;TZID=US-Eastern:19970905T090000
RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR
==> (1997 9:00 AM EDT)September 5;October 3
(1997 9:00 AM EST)November 7;Dec 5
(1998 9:00 AM EST)January 2;February 6;March 6;April 3
(1998 9:00 AM EDT)May 1;June 5
Every 18 months on the 10th thru 15th of the month for 10
occurrences:
DTSTART;TZID=US-Eastern:19970910T090000
RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,
15
==> (1997 9:00 AM EDT)September 10,11,12,13,14,15
(1999 9:00 AM EST)March 10,11,12,13
From an API point of view, I’ve implemented an Rrule class, that takes
as an argument the DTSTART, and the RRULE.
It has one method, each(), which yields a Time for each occurence of the
event, and it mixes in Enumerable.
So, if you want to know if an event occurs on a particular day, you
can do Rrule#detect, if you want all the events in a period, you can do
Rrule.find_all, etc.
I just finished implementing Rrule#each() Monday night, and reading your
email made me hurry to try and use Enumerable to do these things.
I’ve actually run into an interesting problem, which you ruby gurus can
perhaps help me with:
Rrule#each() could yield forever (“every monday”). But the times
generated by Rrule are ordered, so when I do:
rrule = Rrule.new( … every monday…)
and then try to find all mondays in 2005, I want to break when Rrule
starts yielding times in 2006, but how do I do this? If I just call
“break” I find that Enumerable#find_all is returning nil, but I want
the array (possibly empty) of all occurences!
Similar problem with detect, etc., once the times yielded are out of
range, I’m not interested anymore, and want to break.
What to do?
Anyhow, iCalendar supports TODOs and EVENTs, both of which are something
you sound interested in, with recurrence. I’m building an API supporting
this, and I’d be happy to work with you. If you were using the vPim
library it would give me good impetus to keep adding features to it.
Also, your application would be a great use-case, it would allow me to
direct efforts towards implementing the features immediately useable by
you, rather than wondering what some hypothetical user might want.
Personally, my short-term goals are to implement two tools:
-
a command line utility that when I log in lists all the events/todos
in the Apple’s iCal that happen in the next week (because I always
forget to check the calendar)
-
a command line utility that integrates into mutt and allows me to
respond to iCalendar meeting requests and notifications (because I get
these at work, and I’ve no way to respond to them)
This code is Beta! I’m working on it as I write, so don’t rip into it
too badly… but I’d like to hear comments. Particularly about how to
use Enumerable with an #each() that generates an infinite, but sorted,
sequence.
Cheers,
Sam
I can’t send any more info… the mail is to big… Here, at least, is
the Rrule docs:
module Vpim
Implements the iCalendar recurence rule syntax. See etc/rrule.txt for the
syntax description and examples from RFC 2445. The description is pretty
hard to understand, but the examples are more helpful.
···
The implementation is pretty complete, but still lacks support for:
TODO - BYWEEKLY, BYWEEKNO, WKST: rules that recur by the week, or are
limited to particular weeks, not hard, but not trivial, I’ll do it for the
next release
TODO - BYHOUR, BYMINUTE, BYSECOND: trivial to do, but I don’t have an
immediate need for them, I’ll do it for the next release
TODO - BYSETPOS: limiting to only certain recurrences in a set (what does
-1, last occurence, mean for an infinitely occuring rule?)
class Rrule
include Enumerable
# The recurrence rule, +rrule+, specifies how to generate a set of times from
# a start time, +dtstart+. It it is nil or empty, the set contains only +dtstart+.
def initialize(dtstart, rrule = nil)
end
# Yields for each +ytime+ in the recurring set of events.
#
# Warning: the set may be infinite! If you want an upper bound on the
# number of occurences, you need to implement it.
#
# TODO - implement some way of providing this upper-bound to the each, but
# the method has to work with the Enumerable mixin, how do I do that?
def each #:yield: ytime
end
# Iterate over all occurences that overlap with the range [t0, t1]. The
# occurence is considered to have a +duration+, and it is in range unless
# it starts at +t1+ or later, or ends before +t0+.
def each_in_range(t0, t1, duration = 0)
each do |y0|
y1 = y0 + duration
break if y0 >= t1
yield y0 unless y1 < t0
end
end
end
end