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