Search code examples
dateperl

perl's datetime subtraction with sign


I'm trying to use Perl's DateTime to subtract one day from another with a sign. I can get the days in between easily:

sub delta_days {
    my $date1 = shift;
    my $date2 = shift;
    my $d1 = str_to_date_object($date1);
    my $d2 = str_to_date_object($date2);
    return $d2->delta_days($d1)->delta_days();
}
say delta_days('2021-10-21', '1980-8-20');
say delta_days('1980-8-20',  '2021-10-21');

but both of these calls give the difference as 15037, without a sign.

Following the documentation https://metacpan.org/pod/DateTime

I see

# not DST
my $dt1 = DateTime->new(
    year      => 2003,
    month     => 5,
    day       => 6,
    time_zone => 'America/Chicago',
);
 
# is DST
my $dt2 = DateTime->new(
    year      => 2003,
    month     => 11,
    day       => 6,
    time_zone => 'America/Chicago',
);
my $dur = $dt2->subtract_datetime($dt1)->days();

I have checked a similar question in How to make DateTime::Duration output only in days? but I don't see how to get a signed difference there.

How can I get the difference in days with a sign?


Solution

  • Two issues: You're doing the subtraction on dates in the wrong order if you want a negative offset, and with the dates you're testing subtract_datetime() with, the duration object is going to have 0 days.

    Compare with this version:

    #!/usr/bin/env perl
    use strict;
    use warnings;
    use feature qw/say/;
    use DateTime;
    use Data::Dumper;
    
    # not DST
    my $dt1 = DateTime->new(
        year      => 2003,
        month     => 5,
        day       => 6,
        time_zone => 'America/Chicago',
    );
    
    # is DST
    my $dt2 = DateTime->new(
        year      => 2003,
        month     => 11,
        day       => 6,
        time_zone => 'America/Chicago',
        );
    say $dt1;
    say $dt2;
    
    my $dur = $dt1->subtract_datetime($dt2);
    print Dumper({$dur->deltas});
    

    which when run produces

    2003-05-06T00:00:00
    2003-11-06T00:00:00
    $VAR1 = {
              'minutes' => 0,
              'nanoseconds' => 0,
              'months' => -6,
              'seconds' => 0,
              'days' => 0
            };
    

    Note the -6 months offset. DateTime::Duration objects will not convert between months and days according to the documentation so this looks like a dead end.

    The delta_days method you looked at first returns the actual difference in days, but as noted it's an absolute value. You can add a check to convert it to negative if the first date is before the second:

    my $days = $dt1->delta_days($dt2)->in_units('days');
    $days *= -1 if $dt1 < $dt2;
    say $days; # Prints -184
    

    and a variation of your delta_days() function:

    sub delta_days {
        my $date1 = shift;
        my $date2 = shift;
        my $d1 = str_to_date_object($date1);
        my $d2 = str_to_date_object($date2);
        return $d1->delta_days($d2)->in_units('days') * ($d1 < $d2 ? -1 : 1);
    }
    say delta_days('2021-10-21', '1980-8-20');
    say delta_days('1980-8-20',  '2021-10-21');