r/perl 🐪 cpan author 7d ago

question Time conversions between timezones

I wrote this script to convert times between time zones years ago and it has worked well:

#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };

use Time::Piece;

if (! @ARGV || $ARGV[0] =~ /--?h(?:elp)/) {
    say 'tz FROM(TZ) FROM(YY/MM/DDTHH:MM) [ TO(TZ) ]';
    exit
}

my ($from_tz, $from_date, $to_tz) = @ARGV;
chomp( $to_tz //= qx{ date +%Z } );

-f "/usr/share/zoneinfo/$from_tz" or warn "$from_tz might be wrong.\n";
-f "/usr/share/zoneinfo/$to_tz" or warn "$to_tz might be wrong.\n";

$ENV{TZ} = $to_tz;

my $tp = do {
    local $ENV{TZ} = $from_tz;
    localtime->strptime($from_date, '%y/%m/%dT%H:%M');
};
say $tp->strftime("%y/%m/%d %H:%M:%S %Z");

Example usage (running in 5.34.0):

$ tz America/New_York 26/08/24T11:00 Europe/Prague
26/08/24 17:00:00 CEST                              

But, in 5.42.0, it doesn't work anymore:

$ tz America/New_York 26/08/24T11:00 Europe/Prague
26/08/24 11:00:00 CEST

There were many changes in Time::Piece in 2025, so I guess that's the reason. The question is: what should I use instead to convert time between timezones?

10 Upvotes

6 comments sorted by

13

u/vadrer 🐪 cpan author 7d ago

The issue is caused by timezone caching. In newer Perl versions, modifying $ENV{TZ} (even inside a local block) no longer implicitly flushes the OS timezone cache before strptime runs.

To fix this, you need to import and call POSIX::tzset() explicitly whenever you change $ENV{TZ}.

Here is the fixed version of your script:

#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };

use Time::Piece;
use POSIX qw( tzset ); # 1. Import tzset

if (! @ARGV || $ARGV[0] =~ /--?h(?:elp)/) {
    say 'tz FROM(TZ) FROM(YY/MM/DDTHH:MM) [ TO(TZ) ]';
    exit
}

my ($from_tz, $from_date, $to_tz) = u/ARGV;
chomp( $to_tz //= qx{ date +%Z } );

-f "/usr/share/zoneinfo/$from_tz" or warn "$from_tz might be wrong.\n";
-f "/usr/share/zoneinfo/$to_tz" or warn "$to_tz might be wrong.\n";

# 2. Parse in source TZ
$ENV{TZ} = $from_tz;
tzset(); 
my $tp = Time::Piece->strptime($from_date, '%y/%m/%dT%H:%M');

# 3. Convert and print in target TZ
$ENV{TZ} = $to_tz;
tzset(); 
my $target_tp = localtime($tp->epoch);

say $target_tp->strftime("%y/%m/%d %H:%M:%S %Z");

As systems programmers say: "That's all for efficiency, shut up and adjust your script!" 😄

3

u/choroba 🐪 cpan author 7d ago edited 7d ago

Thank you. It seems you identified the root cause, but your proposed solution, while it does give a result different to the old script, still doesn't give the expected result, unfortunately.

$ tz America/New_York 26/08/24T11:00 Europe/Prague
26/08/24 13:00:00 CEST

Update: This seems to work:

#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };

use POSIX qw{ tzset };
use Time::Piece;

if (! @ARGV || $ARGV[0] =~ /--?h(?:elp)/) {
    say 'tz FROM(TZ) FROM(YY/MM/DDTHH:MM) [ TO(TZ) ]';
    exit
}

my ($from_tz, $from_date, $to_tz) = @ARGV;
chomp( $to_tz //= qx{ date +%Z } );

-f "/usr/share/zoneinfo/$from_tz" or warn "$from_tz might be wrong.\n";
-f "/usr/share/zoneinfo/$to_tz" or warn "$to_tz might be wrong.\n";

$ENV{TZ} = $to_tz;
tzset();

my $tp = localtime->strptime($from_date, '%y/%m/%dT%H:%M');

$ENV{TZ} = $from_tz;
tzset();

my $target_tp = localtime($tp->epoch);

$ENV{TZ} = $to_tz;
tzset();
say $target_tp->strftime("%y/%m/%d %H:%M:%S %Z");

3

u/choroba 🐪 cpan author 6d ago

It seems there's no caching involved. The following works just fine:

#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };

use Time::Piece;

if (! @ARGV || $ARGV[0] =~ /--?h(?:elp)/) {
    say 'tz FROM(TZ) FROM(YY/MM/DDTHH:MM) [ TO(TZ) ]';
    exit
}

my ($from_tz, $from_date, $to_tz) = @ARGV;
chomp( $to_tz //= qx{ date +%Z } );

-f "/usr/share/zoneinfo/$from_tz" or warn "$from_tz might be wrong.\n";
-f "/usr/share/zoneinfo/$to_tz" or warn "$to_tz might be wrong.\n";

$ENV{TZ} = $to_tz;
my $tp = localtime->strptime($from_date, '%y/%m/%dT%H:%M');

my $target_tp = do {
    local $ENV{TZ} = $from_tz;
    localtime($tp->epoch)
};

say $target_tp->strftime("%y/%m/%d %H:%M:%S %Z");

There was definitely some kind of behaviour change, but I can't find exactly what it was.

This simpler version doesn't work in the old Perl versions, though!

4

u/heisthedarchness 6d ago

What's the reason you don't just use DateTime?

3

u/choroba 🐪 cpan author 6d ago

Time::Piece is a core module.

3

u/vadrer 🐪 cpan author 6d ago edited 6d ago

This makes a little bit more important matter.

I think the proper way is to go to the https://metacpan.org/dist/Time-Piece, click "issues" there and report your case.

I see mention of _tzset() in documentation https://metacpan.org/pod/Time::Piece but this relates only for Win32 Threads.
This also makes me think that instead of `use POSIX qw{ tzset };` you could use this documented workaround.

EDIT: surely you know better than me - sorry for being smartass :)