datetimeperl

Why does truncating a DateTime to "day" cause me to get the wrong result when subtracting a day?


Given the current date, I need to get yesterday's date using the Perl DateTime module.

The current time of the day doesn't matter. I just need yesterday's date, so I figured I could truncate the time value off of the current day before subtracting a day.

This seems like it should be simple enough, but I end up with the incorrect result.

use DateTime;
use strict;
use warnings;

my $today = DateTime->now(time_zone => 'local');

# Set time zone to UTC before performing math (see DateTime documentation)
$today->set_time_zone('UTC');
my $yesterday = $today->clone->truncate(to => 'day')->subtract(days => 1);

# Set time zone back to local
$today->set_time_zone('local');
$yesterday->set_time_zone('local');

print "YESTERDAY: ".$yesterday->strftime("%m/%d/%Y")."\n";
print "    TODAY: ".$today->strftime("%m/%d/%Y")."\n";

This code outputs:

YESTERDAY: 11/30/2024
    TODAY: 12/02/2024

Note that if I don't ->truncate(to => 'day') I do seem to get the correct result, but I'm still puzzled by the fact that truncating the time off of the date causes the result to change.

Does anyone know why this is happening?


Solution

  • Use floating instead of UTC.


    $today->set_time_zone( $tz ); doesn't simply change the time zone to $tz; it converts the timestamp from its current time zone to the specified time zone. This can result in a date change.

    I'm going to rearrange your code a little so I can better illustrate what is happening. The following is equivalent to what you have, plus an example in the comments:

    my $now_local = DateTime->now( time_zone => 'local' );     # 2024-12-02T01:30:00+02:00
    my $now_utc = $now_local->clone->set_time_zone( 'UTC' );   # 2024-12-01T23:30:00Z
    my $today = $now_utc->clone->truncate( to => 'day' );      # 2024-12-01
    my $yesterday = $today->clone->subtract( days => 1 );      # 2024-11-30
    
    say $now_local->strftime( "%F" );  # 2024-12-02
    say $yesterday->strftime( "%F" );  # 2024-11-30
    

    You should use floating instead of UTC. This effectively removes the time zone. It's like setting the time to UTC, but without converting the time.

    my $now_local = DateTime->now( time_zone => 'local' );     # 2024-12-02T01:30:00+02:00
    my $now = $now_local->clone->set_time_zone( 'floating' );  # 2024-12-02T01:30:00
    my $today = $now_utc->clone->truncate( to => 'day' );      # 2024-12-02
    my $yesterday = $today->clone->subtract( days => 1 );      # 2024-12-01
    
    say $now_local->strftime( "%F" );  # 2024-12-02
    say $yesterday->strftime( "%F" );  # 2024-12-01
    

    Using the floating time zone for date calculations avoids problems with Daylight-Saving Time and other small discontinuities, but it will still fail if a whole day is skipped. This happened in 2011 in Samoa when it changed its offset to be on the other side of the date line. DateTime needs facilities to manipulate local dates to address this issue.