Skip to content

DateTime::Duration::in_units('days') should not return completely wrong answers for durations over 1 month #133

@deven

Description

@deven

Currently, DateTime::Duration::in_units('days') will only return the days portion of the DateTime::Duration object, completely ignoring the number of months that may also be stored. Although this behavior is documented, the caveat is very easy to miss. This method should not return completely wrong answers like this.

Here is an example script which demonstrates the problem, and also shows that DateTime::delta_days() will return the correct number of days (albeit losing the negative sign), unlike DateTime::subtract_datetime():

#!/usr/bin/env perl

use strict;
use warnings;

use Data::Dump qw[dump];
use DateTime;

print "\$DateTime::VERSION = $DateTime::VERSION\n\n";

my $date1 = DateTime->new(year => 2022, month => 8, day => 9);
print "\$date1 = \"$date1\"\n\n";

my $date2 = DateTime->new(year => 2022, month => 6, day => 17);
print "\$date2 = \"$date2\"\n\n";

my $duration = $date2 - $date1;
print "\$duration = \$date2 - \$date1 = \$date2->subtract_datetime(\$date1) = " . dump($duration) . "\n\n";

print "\$duration->in_units(\"days\") = " . $duration->in_units('days') . "\n\n";

print "\$date2->delta_days(\$date1)->in_units(\"days\") = " . $date2->delta_days($date1)->in_units("days") . "\n";

Here is the output generated by the above script with DateTime version 1.58:

$DateTime::VERSION = 1.58

$date1 = "2022-08-09T00:00:00"

$date2 = "2022-06-17T00:00:00"

$duration = $date2 - $date1 = $date2->subtract_datetime($date1) = bless({
  days => -22,
  end_of_month => "preserve",
  minutes => 0,
  months => -1,
  nanoseconds => 0,
  seconds => 0,
}, "DateTime::Duration")

$duration->in_units("days") = -22

$date2->delta_days($date1)->in_units("days") = 53

Returning -22 for $duration->in_units("days") when the correct duration is -53 days is clearly wrong. Several better options come to mind:

  • Ideally, extend DateTime::Duration to be able to store the total number of days, in addition to the breakdown of months and days, so that this call could correctly return -53. (This obviously can't work in cases where a duration was constructed directly as months and days only, but it would solve the problem for durations created by subtracting DateTime objects.)
  • die with an error because this duration is over a month, "months" was not in the list of units passed to in_units(), and there's no way to know the exact number of days which would be correct. This is arguably the right thing to do if the correct value cannot be calculated, but it might cause backwards-compatibility issues.
  • return undef for the same reasons, preferably with a warning message generated. This could also cause backwards-compatibility issues, but likely to a lesser degree.
  • Make a best-effort guess at converting the months to days, using an approximation of either 30.5 days/month or perhaps 30.4375 days/month ((366 + 365*3) / 12), and rounding to the nearest integer. This might return either -52 or -53 here, which might not be exactly correct in all cases, but it would always be close to the correct answer, and it's much better to return a slightly wrong answer of -52 than a very wrong answer of -22. This should be the most backwards-compatible option.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions