Durations, Sets, & Spans

Dave Rolsky edited this page Jan 30, 2017 · 1 revision

What is a DateTime::Duration?

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;

What are the three endofmonth modes in DateTime::Duration?

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;

What are DateTime::Set objects?

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);

How do I get at the dates inside a DateTime::Set?

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.

What are the DateTime::Set set operations?

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.

Is there an easier way to create sets than writing subroutines?

The following modules create some useful common recurrences.

What are DateTime::Span objects?

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);

What are DateTime::SpanSet objects?

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 );