Sample Calculations

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

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 ? ] ( ( FAQ : Basic Usage ) )

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.

# 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 find next Monday's date?

my $next_monday = DateTime->today->add(days => 8 - DateTime->today->day_of_week);

How do I find yesterday's date?

my $yesterday = DateTime->today->subtract(days => 1);

Note the subtle difference between this and the alternative

my $this_moment_one_day_ago = DateTime->now( time_zone => 'local' )->subtract( days => 1 );

In the former the time component is truncated to zero.

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 (link to a non-existent wiki in a page link - FAQ: Durations Sets Spans)

# 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 between two times, but without counting Saturdays and Sundays or calculate business time between two times?

use Time::Business;

my $btime = Time::Business->new(
    {
        WORKDAYS => [ 1, 2, 3, 4, 5 ],
        STARTIME => 900,
        ENDTIME  => 1700,
    }
);

$start   = time();
$end     = time() + 86400;
$seconds = $btime->calctime( $start, $end );

To get the number of business time in days hours minutes you can :

my $bus_string = $btime = workTimeString($seconds)

Which will return something like "1 day 3 hours 22 minutes". Which is 1 day business time where 1 day is STARTIME-ENDTIME (in this case 8 hours).

How can I compare two DateTime::Duration objects?

TODO