-
Notifications
You must be signed in to change notification settings - Fork 49
Durations, Sets, & Spans
A DateTime::Duration represents a period of time. You get DateTime::Duration objects when you subtract one DateTime object from another and you can add a DateTime::Duration to an existing DateTime object to create a new DateTime object.
A DateTime::Duration is broken down into the constituent parts, since adding 31 days may not be the same as adding 1 month, or 60 seconds may not be the same as 1 minute if there are leap seconds.
use DateTime;
my $membership = DateTime::Duration->new( months => 12 );
my $still_a_member =
$membership_start->add_duration($membership) >= DateTime->today
? 1
: 0;
The three modes govern how date overflows are handled when dealing with month or year durations. So if you have the following:
use DateTime::Duration();
sub test_duration_mode {
my ($dt, $mode) = @_;
my $dur = DateTime::Duration->new
(years => 1, end_of_month => $mode);
my $res = $dt + $dur;
print $res->ymd(), "\n";
}
my $dt1 = DateTime->new(year => 2000, month => 2, day => 29);
my $dt2 = DateTime->new(year => 2003, month => 2, day => 28);
# wrap rolls any extra over to the next month
test_duration_mode($dt1, "wrap"); # Prints: "2001-03-01\n"
# limit prevents a rollover
test_duration_mode($dt1, "limit"); # Prints: "2001-02-28\n"
# but will lose the end of monthness after 3 years:
test_duration_mode($dt2, "limit"); # Prints: "2004-02-28\n"
# preserve keeps the value at the end of the month
test_duration_mode($dt1, "preserve"); # Prints: "2001-02-28\n"
# even if it would have fallen slightly short:
test_duration_mode($dt2, "preserve"); # Prints: "2004-02-29\n"
If you want to calculate something like "two days before the end of each" month, you'll want to use a recurrence instead:
# From Flavio Glock
$set = DateTime::Event::Recurrence->monthly( days => -2 );
print "Next occurrence ", $set->next( $dt )->datetime;
A DateTime::Set is an efficient representation of a number of DateTime objects. You can either create them from a list of existing DateTime objects:
use DateTime::Set;
my $dt1 = DateTime->new(year => 2003, month => 6, day => 1);
my $dt2 = DateTime->new(year => 2003, month => 3, day => 1);
my $dt3 = DateTime->new(year => 2003, month => 3, day => 2);
my $set1 = DateTime::Set->from_datetimes( dates => [ $dt1, $dt2 ] );
$set1 = $set1->union($dt3); # Add in another date
print "Min of the set is the lowest date\n" if $dt2 == $set1->min();
print "Max of the set is the highest date\n" if $dt1 == $set1->max();
my $it = $set1->iterator();
while ( my $dt = $it->next() ) {
print $dt->ymd(), "\n";
}
# Prints: "2003-03-01\n2003-03-02\n2003-06-01\n"
Or DateTime::Set can handle sets that do not fully exist. For instance you could make a set that represents the first of every month:
my $set = DateTime::Set->from_recurrence(
recurrence => sub {
$_[0]->truncate( to => 'month' )->add( months => 1 )
});
my $dt1 = DateTime->new(year => 2003, month => 3, day => 1);
my $dt2 = DateTime->new(year => 2003, month => 2, day => 11);
print "2003-03-01 is the first of the month\n"
if $set->contains($dt1);
print "2003-03-01 is not the first of the month\n"
unless $set->contains($dt2);
You can use contains() to see if a given date is in the set as shown in What are DateTime::Set objects? or you can use an iterator to loop over all values in the set.
To iterate over a set you need to make sure that the start date of the set is defined (and if you want the iterator to ever finish you need to make sure that there is an end date. If your set does not have one yet, you can either create a new DateTime::Set or a DateTime::Span and take the intersection of the set. As a convenience, the iterator() method takes the same arguments as DateTime::Span and will use them to limit the iteration as if the corresponding span were used.
In the following example we use DateTime::Event::Recurrence to more easily define a monthly recurrence that is equivalent to the one we defined manually in What are DateTime::Set objects?.
use DateTime::Event::Recurrence;
my $set = DateTime::Event::Recurrence->monthly();
my $dt1 = DateTime->new(year => 2003, month => 3, day => 2);
my $dt2 = DateTime->new(year => 2003, month => 6, day => 1);
# Unlimited iterator on an unbounded set
my $it1 = $set->iterator();
my $next = $it1->next();
print "-inf\n" if $next->is_infinite && $next < DateTime->today; # Prints: "-inf\n"
# Limited iterator on an unbounded set
my $it2 = $set->iterator(start => $dt1, end => $dt2);
while ( $dt = $it2->previous() ) {
print $dt->ymd(), "\n";
}
# Prints: "2003-06-01\n2003-05-01\n2003-04-01\n"
In the previous example we used the method previous() to iterate over a set from the highest date to the lowest.
Or you can turn a DateTime::Set into a simple list of DateTime objects using the as_list method. If possible you should avoid doing this because the DateTime::Set representation is far more efficient.
One of the most important features of DateTime::Set is that you can perform set operations. For instance you can take a set representing the first day in each month and intersect it with a set representing Mondays and the resultant set would give you the dates where Monday is the first day of the month:
use DateTime::Event::Recurrence;
# First of the month
my $fom = DateTime::Event::Recurrence->monthly();
# Every Monday (first day of the week)
my $mon = DateTime::Event::Recurrence->weekly( days => 1 );
# Every Monday that is the start of a month
my $set = $fom->intersection($mon);
my $it = $set->iterator
(start =>
DateTime->new(year => 2003, month => 1, day => 1),
before =>
DateTime->new(year => 2004, month => 1, day => 1));
while ( my $dt = $it->previous() ) {
print $dt->ymd(), "\n";
}
# Prints: "2003-12-01\n2003-09-01\n"
The complete list of set operations is:
- $set3 = $set1->union($set2) - $set3 will contain all items from $set1 and $set2.
- $set3 = $set1->complement($set2) - $set3 will contain only the items from $set1 that are not in $set2.
- $set3 = $set1->intersection($set2) - $set3 will contain only the items from $set1 that are in $set2.
The last operator, unary complement $set3 = $set1-complement() returns all of the items that do not exist in $set1 as a DateTime::SpanSet.
The following modules create some useful common recurrences.
- DateTime::Event::Recurrence - Creates DateTime::Sets for each unit of time (day, hour, etc.) and allows you to customize them.
- DateTime::Event::Cron - Creates DateTime::Set objects using the crontab syntax.
- DateTime::Event::ICal - Creates DateTime::Set objects (and other objects) using the powerful but complex iCal syntax.
A DateTime::Span represents an event that occurs over a range of time rather than a DateTime which really is a point event (although a DateTime can be used to represent a span if you truncate the objects to the same resolution). Unlike DateTime::Duration objects they have fixed start points and ending points.
For example, a span might represent a meeting scheduled to occur from 2003-03-03 12:00:00 to 2003-03-03 13:00:00. The span includes every possible point in time for that hour. Whether or not it includes its start and end times is something that you can set when constructing a span object.
use DateTime;
use DateTime::Span;
my $start = DateTime->new( year => 2003, month => 3, day => 3, hour => 12 );
my $before = DateTime->new( year => 2003, month => 3, day => 3, hour => 13 );
# includes start time but not end time
my $meeting =
DateTime::Span->from_datetimes( start => $start, before => $before );
my $dt = DateTime->new( year => 2003, month => 3, day => 3,
hour => 12, minute => 15 );
print "I am going to be busy then" if $meeting->contains($dt);
A DateTime::SpanSet represents a set of DateTime::Span objects. For example you could represent the stylized working week of 9-5, M-F with 12-1 as lunch break (ignoring holidays) as follows:
use DateTime::Event::Recurrence;
use DateTime::SpanSet;
# Make the set representing the work start times: M-F 9:00 and 13:00
my $start = DateTime::Event::Recurrence->weekly
( days => [1 .. 5], hours => [8, 13] );
# Make the set representing the work end times: M-F 12:00 and 17:00
my $end = DateTime::Event::Recurrence->weekly
( days => [1 .. 5], hours => [12, 17] );
# Build a spanset from the set of starting points and ending points
my $spanset = DateTime::SpanSet->from_sets
( start_set => $start,
end_set => $end );
# Iterate from Thursday the 3rd to Monday the 6th
my $it = $spanset->iterator
(start =>
DateTime->new(year => 2003, month => 1, day => 3),
before =>
DateTime->new(year => 2003, month => 1, day => 7));
while (my $span = $it->next) {
my ($st, $end) = ($span->start(), $span->end());
print $st->day_abbr, " ", $st->hour, " to ", $end->hour, "\n";
}
# Prints: "Fri 8 to 12\nFri 13 to 17\nMon 8 to 12\nMon 13 to 17\n"
# Now see if a given DateTime falls within working hours
my $dt = DateTime->new(year => 2003, month => 2, day => 11, hour => 11);
print $dt->datetime, " is a work time\n"
if $spanset->contains( $dt );