DateTime Title

Sample Date Calculations

How do I check whether a given date lies within a certain range of days?

my $dt1  = DateTime->new(year => 2002, month => 3, day => 1);
my $dt2  = DateTime->new(year => 2002, month => 2, day => 11);
my $date = DateTime->new(year => 2002, month => 2, day => 23);

# Make sure $dt1 is less than $dt2
($dt1, $dt2) = ($dt2, $dt1) if $dt1 > $dt2;

# Truncate all dates to day resolution (skip this if you want
# to compare exact times)
$dt1->truncate(  to => 'day' );
$dt1->truncate(  to => 'day' );
$date->truncate( to => 'day' );

# Now do the comparison
if ($dt1 <= $date and $date <= $dt2) {
  print '$date is between the given dates';
}

Or you can do it using DateTime::Span:

use DateTime::Span;

my $dt1  = DateTime->new(year => 2002, month => 3, day => 1);
my $dt2  = DateTime->new(year => 2002, month => 2, day => 11);
my $date = DateTime->new(year => 2002, month => 2, day => 23);

# Make sure $dt1 is less than $dt2
($dt1, $dt2) = ($dt2, $dt1) if $dt1 > $dt2;

# Make the span (use after and before if you want > and < rather
# than the >= and <= that start and end give)
my $span = DateTime::Span->from_datetimes(start => $dt1,
                                          end   => $dt2);
if ($span->contains($date)) {
  print '$date is between the given dates';
}

See also Why do I need to truncate dates?

How do I check whether two dates and times lie more or less than a given time interval apart?

use DateTime::Duration;

my $dt1  = DateTime->new(year => 2002, month => 3, day => 1);
my $dt2  = DateTime->new(year => 2002, month => 2, day => 11);

# Make a duration object to represent the interval
$interval =
  DateTime::Duration->new( days => 19, hours => 3, minutes => 12);

sub within_interval {
    my ($dt1, $dt2, $interval) = @_;

    # Make sure $dt1 is less than $dt2
    ($dt1, $dt2) = ($dt2, $dt1) if $dt1 > $dt2;

    # If the older date is more recent than the newer date once we
    # subtract the interval then the dates are closer than the
    # interval
    if ($dt2 - $interval < $dt1) {
        return 1;
    } else {
        return 0;
    }
}

print 'closer than $interval'
   if within_interval($dt1, $dt2, $interval);

How do I verify whether someone has a certain age?

This is just an application of the How do I check whether two dates and times lie more or less than a given time interval apart?

Note that simply subtracting the dates and looking at the year component will not work. See How do I compare DateTime::Duration objects?

# Build a date representing their birthday
my $birthday = DateTime->new(year => 1974, month  => 2, day => 11,
                             hour => 6,    minute => 14);

# Make sure we are comparing apples to apples by truncating to days
# since you don't have to be 18 exactly by the minute, just to the day
$birthday->truncate( to => 'day' );
my $today = DateTime->today();

# Represent the range we care about
my $age_18 = DateTime::Duration->new( years => 18 );

print "You may be able to drink or vote..."
   unless within_interval($birthday, $today, $age_18);

How can I calculate the day of the week if I want to consider Sunday the first day of the week?

sub my_day_of_week {
     my $dt = shift;

     my $dow = ($dt->day_of_week + 1);
     return $dow > 7 ? $dow % 7 : $dow;
}

How do I calculate the number of the week of month the given date lies in?

For example:

        April 1998
Mon Tue Wed Thu Fri Sat Sun
          1   2   3   4   5  =  week #1
  6   7   8   9  10  11  12  =  week #2
 13  14  15  16  17  18  19  =  week #3
 20  21  22  23  24  25  26  =  week #4
 27  28  29  30              =  week #5

Note that this is different from the ISO8601 definition of "weeks of the month". The ISO definition says that the first week containing a Thursday is week #1. This means that if the month starts on a Friday, then the first three days of that month (Friday through Sunday) are week #0. The DateTime module has a week_of_month() method which returns the ISO week number for the date.

# Takes as arguments:
#  - The date
#  - The day that we want to call the start of the week (1 is Monday, 7
#    Sunday) (optional)
sub get_week_num {
  my $dt            = shift;
  my $start_of_week = shift || 1;

  # Work out what day the first of the month falls on
  my $first = $dt->clone();
  $first->set(day => 1);
  my $wday  = $first->day_of_week();

  # And adjust the day to the start of the week
  $wday = ($wday - $start_of_week + 7) % 7;

  # Then do the calculation to work out the week
  my $mday  = $dt->day_of_month_0();

  return int ( ($mday + $wday) / 7 ) + 1;
}

How do I calculate the date of the Wednesday of the same week as the current date?

# Takes as arguments:
#  - The date
#  - The target day (1 is Monday, 7 Sunday)
#  - The day that we want to call the start of the week (1 is Monday, 7
#    Sunday) (optional)
# NOTE: This may end up in a different month...
sub get_day_in_same_week {
  my $dt            = shift;
  my $target        = shift;
  my $start_of_week = shift || 1;

  # Work out what day the date is within the (corrected) week
  my $wday = ($dt->day_of_week() - $start_of_week + 7) % 7;

  # Correct the argument day to our week
  $target = ($target - $start_of_week + 7) % 7;

  # Then adjust the current day
  return $dt->clone()->add(days => $target - $wday);
}

How do I calculate the last and the next Saturday for any given date?

# The date and target (1 is Monday, 7 Sunday)
my $dt = DateTime->new(year => 1998, month => 4, day => 3); # Friday
my $target = 6; # Saturday

# Get the day of the week for the given date
my $dow = $dt->day_of_week();

# Apply the corrections
my ($prev, $next) = ($dt->clone(), $dt->clone());

if ($dow == $target) {
    $prev->add( days => -7 );
    $next->add( days =>  7 );
} else {
    my $correction = ( $target - $dow + 7 ) % 7;
    $prev->add( days => $correction - 7 );
    $next->add( days => $correction );
}

# $prev is 1998-03-28, $next is 1998-04-04

How can I calculate the last business day (payday!) of a month?

Start from the end of the month and then work backwards until we reach a weekday.

my $dt = DateTime->last_day_of_month( year => 2003, month => 8 );

# day 6 is Saturday, day 7 is Sunday
while ( $dt->day_of_week >= 6 ) { $dt->subtract( days => 1 ) }

print "Payday is ", $dt->ymd, "\n";

This isn't the most efficient solution, but it's easy to understand.

How can I find what day the third Friday of a month is on?

# Define the meeting time and a date in the current month
my $meeting_day  = 5; # (1 is Monday, 7 is Sunday)
my $meeting_week = 3;
my $dt = DateTime->new(year => 1998, month => 4, day => 4);

# Get the first of the month we care about
my $result = $dt->clone()->set( day => 1 );

# Adjust the result to the correct day of the week and adjust the
# weeks
my $dow = $result->day_of_week();
$result->add( days => ( $meeting_day - $dow + 7 ) % 7,
              weeks => $meeting_week - 1 );

# See if we went to the next month
die "There is no matching date in the month"
   if $dt->month() != $result->month();

# $result is now 1998-4-17

How can I iterate through a range of dates?

The following recipe assumes that you have 2 dates and want to loop over them. An alternate way would be to create a DateTime::Set and iterate over it.

my $start_dt = DateTime->new(year => 1998, month  => 4,  day => 7);
my $end_dt   = DateTime->new(year => 1998, month  => 7,  day => 7);

my $weeks = 0;
for (my $dt = $start_dt->clone();
     $dt <= $end_dt;
     $dt->add(weeks => 1) ) {

  $weeks++;
}

How can I create a list of dates in a certain range?

There are a few ways to do this, you can create a list of DateTime objects, create a DateTime::Set object that represents the list, or simply use the iterator from question How can I iterate through a range of dates?.

Of the three choices, the simple iteration is probably fastest, but you can not easily pass the list around. If you need to pass a list of dates around then DateTime::Set is the way to go since it doesn't generate the dates until they are needed and you can easily augment or filter the list. See What are DateTime::Set objects?

# As a Perl list
my $start_dt = DateTime->new(year => 1998, month  => 4,  day => 7);
my $end_dt   = DateTime->new(year => 1998, month  => 7,  day => 7);

my @list = ();
for (my $dt = $start_dt->clone();
     $dt <= $end_dt;
     $dt->add(weeks => 1) )
{
  push @list, $dt->clone();
}

# As a DateTime::Set.  We use DateTime::Event::Recurrence to easily
# create the sets (see also DateTime::Event::ICal for more
# complicated sets)
use DateTime::Event::Recurrence;
use DateTime::Span;
my $set = DateTime::Event::Recurrence->daily(start    => $start_dt,
                                             interval => 7);
$set = $set->intersection(DateTime::Span->from_datetimes
                              (start => $start_dt, end => $end_dt ));

How can I calculate the difference in days between dates

You need to use delta_days(). Twice.

my $dt1 = DateTime->new( year => 2009, month => 1, day => 1 );
my $dt2 = DateTime->new( year => 2010, month => 1, day => 1 );
my $days = $dt1->delta_days($dt2)->delta_days;
# $days becomes 365

How can I calculate the difference in days between dates, but without counting Saturdays and Sundays?

TODO

How can I compare two DateTime::Duration objects?

TODO