NAME Date::Set - Date set math SYNOPSIS use Date::Set; $a = Date::Set->event( at => '20020311' ); # 20020311 $a->event( at => [ '20020312', '20020313' ] ); # 20020311,[20020312..20020313] $a->exclude( at => '20020312' ); # 20020311,(20020312..20020313] DESCRIPTION Date::Set is a module for date/time sets. It allows you to generate groups of dates, like "every wednesday", and then find all the dates matching that pattern. This module is part of the Reefknot project http://reefknot.sf.net It requires Date::ICal and Set::Infinite. Limitations THIS IS PRELIMINARY INFORMATION. This API may change. Everything in 'OLD API' section is safe to use, but might get deprecated. Some internal operations still use the system's 'time' functions and are limited by epoch issues (no support for years outside the 1970-2038 range). Date::Set does not implement timezones yet. All dates are in UTC. Date::ICal durations are not supported yet. IETF RFC 2445 (iCalendar) If you want to understand the context of this module, look at IETF RFC 2445 (iCalendar). It specifies the syntax for describing recurring events. If you don't need iCalendar functionality, you may try to use Set::Infinite directly. Most of Date::Set is syntactic sugar for Set::Infinite functions. RFC2445 can be obtained for free at http://www.ietf.org/rfc/rfc2445.txt ISO 8601 week We use the words 'weekyear' and 'year' with special meanings. 'year' is a period beginning in january first, ending in december 31. 'weekyear' is the year, beginning in 'first week of year' and ending in 'last week of year'. This year break is somewhere in late-december or begin-january, and it is NOT equal to 'first day of january'. However, 'first monday of year' is 'first monday of january'. It is not 'first monday of first week'. ISO8601 cannot be obtained for free, as far as I know. What's a Date Set A Date Set is a collection of Dates. Date::ICal module defines what a 'date' is. Set::Infinite module defines what a 'set' is. This module puts them together. This module accepts both Date::ICal objects or string dates. These are Date Sets: '' # empty '19971024T120000Z' # one date '19971024T120000Z', '19971025T120000Z' # two dates Period A Date Set period is an infinite set: you can't count how many single dates are there inside the set, because it is 'continuous': '19971024T120000Z' ... '19971025T120000Z' # all dates between days 24 and 25 '19971024T120000Z' ... 'infinity' # all dates after day 24 A Date Set can have more date periods: '19971024T120000Z' ... '19971025T120000Z', # all dates between days 24 '19971124T120000Z' ... '19971125T120000Z' # and 25, in october and in november Recurrence Sometimes a Date::Set have an infinity number of periods. This is what happen when you have a 'recurrence'. A recurrence is created by a 'recurrence rule': $recurr = Date::Set->event( rule = 'FREQ=YEARLY' ); # all possible years An unbounded recurrence like this cannot be printed. It would take an infinitely long roll of paper. print $recurr; # "Too Complex" You can limit a recurrence into a more useful period of time: $a->event( rule => 'FREQ=YEARLY;INTERVAL=2' ); $a->during( start => $today, end => $year_2020 ); The program waits until you ask for a particular recurrence before calculating it. This is implemented by module Set::Infinite, and it is based on 'functional programming'. If you are interested on how this works, take a look at Set::Infinite. Encapsulation Object-oriented programmers are told not to modify an object's data directly, and to use the object's methods instead. For most objects you don't see any difference, but for Date::Set objects, changing the internal data might break your program: - there are many internal formats/states for Date::Set objects, that are translated by the API at run-time (this behaviour is inherited from Set::Infinite). You must be sure what your object state will be. In other words, you might be asking for data that does not exist yet. - due to optimizations, modifying an object's internal data might break some function's results, since you might get a pointer into the memoization cache. In other words, two different objects might be sharing the same data. Open and closed intervals If we used integer arithmetic only, then the interval '20010101T000000' < date < '20020501T000000' could be written [ '20010101T000001' .. '20020430T235959' ]. This method doesn't work well for real numbers, so we use the 'open' and 'closed' interval notation: A closed interval is an interval which includes its limit points. It is written with square brackets. [ '20010101' .. '20020501' ] # '20010101' <= date <= '20020501' If you remove '20020501' from the interval, you get a half-open interval. The open side is written with parenthesis. [ '20010101' .. '20020501' ) # '20010101' <= date < '20020501' If you remove '20010101' and '20020501' from the interval, you get an open interval. ( '20010101' .. '20020501' ) # '20010101' < date < '20020501' "NEW" API SUBROUTINE METHODS These methods perform everything as side-effects to the object's data structures. They return the object itself, modified. event HASH $a->event ( rule => $rule ); $a->event ( at => $date ); $a->event ( start => $start, end => $end ); $a = Date::Set->event ( at => $date ); # constructor Timeline diagram to explain 'event' effect parameter contents: $a = .........[**************]................... # period $b = ................[****************].......... # period $c = ............................[***********]... # period $a->event( at => $b ) $a = .........[***********************].......... # bigger period $a->event( at => $c ) $a = .........[**************]...[***********]... # two periods Inserts events in a Date::Set. Use 'event' to create or enlarge a Set. Calling 'event' without parameters returns 'forever', that is: (-Inf .. Inf) rule adds the dates from a recurrence rule, as defined in RFC2445. This is a simple list of dates. These dates are not 'periods', they have no duration. $a->event( rule => 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3' ); Optimization tip: rules that have start/end dates might execute faster. A rule might have a DTSTART: $a->event( rule => 'DTSTART=19990101Z;FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3' ); $a->event( dtstart => '19990101Z', rule => 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3' ); at adds more dates or periods to the set $a->event( at => '19971024T120000Z' ); # one event $a->event( at => [ '19971024T120000Z', '19971025T120000Z' ] ); # a period $a->event( at => $set ); # one Date::Set $a->event( at => [ $set1, $set2 ] ); # two Date::Sets $a->event( at => [ [ '19971024T120000Z', '19971025T120000Z' ] ] ); # one period $a->event( at => [ [ '19971024T120000Z', '19971025T120000Z' ], [ '19971027T120000Z', '19971028T120000Z' ] ] ); # two periods If 'rule' is used together with 'at' it will add the recurring events that are inside that period only. The period is a 'boundary': $a->event( rule => 'FREQ=YEARLY', at => [ [ '20010101', '20030101' ] ] ); # 2001, 2002, 2003 start, end add a time period to the set: $a->event( start => '19971024T120000Z' ); # one period that goes forever until +infinity $a->event( end => '19971025T120000Z' ); # one period that existed forever since -infinity $a->event( start => '19971024T120000Z', end => '19971025T120000Z' ); # one period if 'at' is used together with 'start'/'end' it will add the periods that are inside that boundaries only: $a->event( at => [ [ '20010101', '20090101' ] ], end => '20020101' ); # period starting 2001, ending 2002 $a->event( at => [ [ '20010101', '20090101' ] ], start => '20070101' ); # period starting 2007, ending 2009 if 'rule' is used together with 'start'/'end' it will add the recurring events that are inside that boundaries only: $a->event( rule => 'FREQ=YEARLY', start => '20010101', end => '20030101' ); # 2001, 2002, 2003 you can mix 'at' and 'start'/'end' boundary effects to 'rule': $a->event( rule => 'FREQ=YEARLY', at => [ [ '20010101', '20090101' ] ], end => '20020101' ); # 2001, 2002 Timeline diagram to explain 'event' effect with bounded recurrence rule parameter contents: $a = .........[**************]................... # period $b = ................[****************].......... # period $r = ...*...*...*...*...*...*...*...*...*...*...* # unbounded recurrence rule $a->event( rule => $r, at => $b ) $a = .........[**************]..*...*............ # period and two occurrences exclude HASH during HASH $a->exclude ( at => $date ); $a->exclude ( rule => $rule ); $a->exclude ( start => $start, end => $end ); $a->during ( at => $date ); $a->during ( rule => $rule ); $a->during ( start => $start, end => $end ); Timeline diagram to explain 'exclude' and 'during' effect parameter contents: $a = .........[**************]................... $b = ................[****************].......... $a->exclude( at => $b ) $a = .........[******)........................... $a->during( at => $b ) $a = ................[*******]................... Calling 'exclude' or 'during' without parameters returns 'never', that is: () the empty set. 'exclude' excludes events from a Date::Set 'during' put start/end boundaries on a Date::Set In other words: 'exclude' cuts out everything that MATCH it, and 'during' cuts out everything that DON'T match it. Use 'exclude' and 'during' to limit a Set size. You can use 'exclude' and 'during' to put boundaries on an infinitely recurring Set. at $a->exclude( at => '19971024T120000Z' ); $a->exclude( at => $set ); $a->during( at => [ '19971024T120000Z', '19971025T120000Z' ] ); # a period $a->during( at => [ $set1, $set2 ] ); 'exclude at' deletes these dates from the set 'during at' limits the set to these boundaries only. rule a recurrence rule as defined in RFC2445 $a->exclude( rule => 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3' ); $a->during( rule => $rule1 ); 'exclude rule' deletes from the set all the dates defined by the rule. The RFC 2445 states that the DTSTART date will not be excluded by a rule, so it isn't. 'during rule' limits the set to the dates defined by the rule. If the set does not contain any of the dates, it gets empty start, end a time period $a->exclude( start => '19971024T120000Z', end => '19971025T120000Z' ); $a->during( start => '19971024T120000Z' ); # limit to forever from start until +infinity $a->exclude( end => '19971025T120000Z' ); # delete everything since -infinity until end 'exclude start' deletes from the set all dates including 'start' and after it 'exclude end' deletes from the set all dates before and including 'end' 'exclude start,end' deletes from the set all dates between and including 'start' and 'end' 'during start' limits the set to the dates including 'start' and after it. If there are no dates after 'start', the set gets empty. 'during end' limits the set to the dates before and including 'end'. If there are no dates before 'end', the set gets empty. 'during start,end' limits the set to the dates between and including 'start' and 'end'. If there are no dates in that period, the set gets empty. wkst STRING Date::Set::wkst('SU'); Sets/reads the module's global "week start day". The parameter must be one of 'MO' (default), 'TU', 'WE', 'TH', 'FR', 'SA', or 'SU'. The effect if to change the 'week' boundaries. It also changes when the first week of year begins, affecting 'weekyear' operations. It has no effect on 'weekday' operations, like 'first tuesday of month' or 'last friday of year'. Return value is current wkst value. FUNCTION METHODS These methods perform operations and return the changed data. They return a new object. The original object is never modified. There are no side-effects. fevent, fexclude, fduring HASH $b = $a->fevent ( at => $date, date_set => $set, rule => $rule, start => $start, end => $end ); Functions equivalent to event() , exclude() , and during() subroutines. These functions return a new Date::Set. They DON'T MODIFY the object, as the subroutines event/exclude/during do. $b = $a->fevent ( at => $date ); is the same as: $b = $a->copy; $b->event ( at => $date ); "OLD" API period Deprecated. Replaced by 'event'. period( time => [time1, time2] ) or period( start => Date::ICal, end => Date::ICal ) This routine is a constructor. Returns a time period bounded by the dates specified when called in a scalar context. dtstart Sets DTSTART time. dtstart( start => time1 ) Returns set intersection [time1 .. Inf) time1 is added to the set. 'dtstart' puts a limit on when the event starts. If the event already starts AFTER dtstart, it will not change. This is a function. It doesn't change the object. dtend Deprecated. Replaced by 'event/during'. dtend( end => time1 ) Returns set intersection (Inf .. time1] 'dtend' puts a limit on when the event finishes. If the event already finish BEFORE dtend, it will not change. duration duration( unit => 'months', duration => 10 ) All intervals are modified to 'duration'. 'unit' parameter can be years, months, days, weeks, hours, minutes, or seconds. recur_by_rule Deprecated. Replaced by 'event'. exclude_by_rule Deprecated. Replaced by 'exclude'. recur_by_date Deprecated. Replaced by 'event'. exclude_by_date Deprecated. Replaced by 'exclude'. occurrences Deprecated. Replaced by 'during'. next_year, next_month, next_week, next_day, next_hour, next_minute, next_weekyear ($date_set) this_year, this_month, this_week, this_day, this_hour, this_minute, this_weekyear ($date_set) prev_year, prev_month, prev_week, prev_day, prev_hour, prev_minute, prev_weekyear ($date_set) $next = next_month( $date_set ) $whole = this_year ( $date_set ) # [20010101..20020101) Returns the next/prev/this unit of time for a given period. It answers questions like, "when is next month for the given period?", "which years are covered by this period?" as_years, as_months, as_weeks, as_days, as_hours, as_minutes, as_weekyears ($date_set) as_months( date-set ) as_weeks ( date-set ) Returns the given period in a 'unit of time' form. It answers questions like, "which months we have in this period?", "which years are covered by this period?" See also previous note on 'weekyear' in 'About ISO 8601 week'. INHERITED 'SET LOGIC' FUNCTIONS These methods are inherited from Set::Infinite. intersects $logic = $a->intersects($b); contains $logic = $a->contains($b); is_null $logic = $a->is_null; is_too_complex Sometimes a set might be too complex to print. It will happen when you ask for 'every year' (a recurrence) but don't specify a starting and ending date. $recurr = Date::Set->event( rule = 'FREQ=YEARLY' ); print $recurr; # "Too Complex" print $recurr->is_too_complex; # "1" $recurr->during( start => '20020101', end => '20050101' ); print $recurr; # "20020101,20030101,20040101,20050101" print $recurr->is_too_complex; # "0" INHERITED 'SET' FUNCTIONS These methods are inherited from Set::Infinite. union $i = $a->union($b); intersection $i = $a->intersection($b); complement $i = $a->complement; $i = $a->complement($b); INHERITED 'SPECIAL' FUNCTIONS These methods are inherited from Set::Infinite. min, max Returns the 'begin' or 'end' of a set. 'date_ical' function returns the actual Date::ICal object they point to. $date1 = $set1->min->date_ical; # the first Date::ICal object in the set $date2 = $set1->max->date_ical; # the last Date::ICal object in the set Warning: modifying an object data might break your program. list Splits a set in simpler, 1-period sets. print $set1; # [20010101..20020101],[20030101..20040101] @subset = $set1->list; print $subset[0]; # [20010101..20020101] print $subset[1]; # [20030101..20040101] print $subset[0]->min->date_ical; # 20010101 print $subset[0]->max->date_ical; # 20020101 This shortcut might work for simple sets, but you should avoid it: print $set1->{list}->[0]->min->date_ical; # 20010101 - DON'T DO THIS Complex sets might take a long time (and a lot memory) to 'list'. Unbounded recurrences should not be list'ed, because they generate infinite or even invalid (empty) lists. If you are not sure what type of set you have, you can test it with is_too_complex() function. size offset select quantize iterate new See Set::Infinite documentation. copy $b = $a->copy; Returns a copy of the object. This is useful if you want to use one of the subroutine methods, without changing the original object. COOKBOOK Create a new, empty set $a = Date::Set->new(); $a = Date::Set->event( at => [] ); $a = Date::Set->during(); Exclude a date from a set TODO Adding a whole year $year = Date::Set->event( at => '20020101' ); $a->event( at => ( $year->as_years ) ); This is not the same thing, since it includes a bit of next year: $a->event( start => '20020101', end => '20030101' ); This is not the same thing, since it misses a bit of this year (a fraction of last second): $a->event( start => '20020101', end => '20021231T235959' ); Using 'during' and 'exclude' to put boundaries on a recurrence $a->event( rule => 'FREQ=YEARLY;INTERVAL=2' ); $a->during( start => $today, end => $year_2020 ); $a->event( rule => 'FREQ=YEARLY;INTERVAL=2' ); $a->exclude( end => $today); $a->exclude( start => $year_2020 ); Application of this/next/prev TODO API INSTABILITIES These are more likely to change: - Some method and parameter names may change if we can find better names. - support to next/prev/this and as_xxx MAY be deleted in future versions if they don't prove to be useful. - 'duration' and 'period' methods MAY change in future versions, to generate open-ended sets. Possibly by using parameter names 'after' and 'before' instead of 'start' and 'end' - accepting timezones - use more of Date::ICal methods for time calculations Some behaviour is yet undefined: - what happens when asked for '31st day of month', when month has less than 31 days? - does it work when using fractional seconds? These might change, but they are not likely: - Accepting string dates MAY be deleted in future versions. AUTHOR Flavio Soibelmann Glock with the Reefknot team. Jesse <>, srl <>, and Mike Heins <> contribute on coding style, documentation, and testing.