13

I need to get the UTC offset of the current time zone in Perl in a cross platform (Windows and various flavors of Unix) way. It should meet this format:

zzzzzz, which represents ±hh:mm in relation to UTC

It looks like I should be able to get it via strftime(), but it doesn't appear to be consistent.

Unix:

Input: perl -MPOSIX -e "print strftime(\"%z\", localtime());"
Output: -0700

Windows:

Input: perl -MPOSIX -e "print strftime(\"%z\", localtime());"
Output: Mountain Standard Time

While it appears that Unix is giving me what I want (or at least something close), Windows is not. I'm pretty sure I can do it with Date::Time or similar, but I'd really like to not have any dependencies that I can't guarantee a user will have due to our wide install base.

Am I missing something obvious here? Thanks in advance.

Morinar
  • 3,460
  • 8
  • 40
  • 58

5 Answers5

17

Time::Local should do the trick

use Time::Local;
@t = localtime(time);
$gmt_offset_in_seconds = timegm(@t) - timelocal(@t);
mob
  • 117,087
  • 18
  • 149
  • 283
2

Here is a portable solution using only the core POSIX module:

perl -MPOSIX -e 'my $tz = (localtime time)[8] * 60 - mktime(gmtime 0) / 60; printf "%+03d:%02d\n", $tz / 60, abs($tz) % 60;'

Bonus: The following subroutine will return a full timestamp with time zone offset and microseconds, as in "YYYY-MM-DD HH:MM:SS.nnnnnn [+-]HHMM":

use POSIX qw[mktime strftime];
use Time::HiRes qw[gettimeofday];

sub timestamp () {
  my @now = gettimeofday;
  my $tz  = (localtime $now[0])[8] * 60 - mktime(gmtime 0) / 60;
  my $ts  = strftime("%Y-%m-%d %H:%M:%S", localtime $now[0]);
  return sprintf "%s.%06d %+03d%02d", $ts, $now[1], $tz / 60, abs($tz) % 60;
}
0

You can compute the difference between localtime($t) and gmtime($t). Here is my version inspired by mob's answer:

use strict;
use warnings;    

sub tz_offset
{
    my $t = shift;
    my @l = localtime($t);
    my @g = gmtime($t);

    my $minutes = ($l[2] - $g[2] + ((($l[5]<<9)|$l[7]) <=> (($g[5]<<9)|$g[7])) * 24) * 60 + $l[1] - $g[1];
    return $minutes unless wantarray;
    return (int($minutes / 60), $minutes % 60);
}

push @ARGV, time;
foreach my $t (@ARGV) {
    printf "%s (%d): %+03d%02u\n", scalar localtime($t), $t, tz_offset($t);
}
Community
  • 1
  • 1
dolmen
  • 8,126
  • 5
  • 40
  • 42
0

An alternative is to use Time::Piece like below, docs here

my $offsetinsecs = localtime(time)->tzoffset;
my $datetimetz = sprintf("%+2d:%02d", $datetimetz, $offsetinsecs/3600, abs($offsetinsecs/60%60));

One way to manually test is by changing the tz

use POSIX qw(tzset);
$ENV{TZ} = 'America/Los_Angeles';

related stackoverflow anser: https://stackoverflow.com/a/56396873/11337921

nicolauscg
  • 115
  • 1
  • 5
-2

A portable way is to compare the output of localtime with gmtime

    $t = time;
    @a = localtime($t);
    @b = gmtime($t);

    $hh = $a[2] - $b[2];
    $mm = $a[1] - $b[1];
    # in the unlikely event that localtime and gmtime are in different years
    if ($a[5]*366+$a[4]*31+$a[3] > $b[5]*366+$b[4]*31+$b[3]) {
      $hh += 24;
    } elsif ($a[5]*366+$a[4]*31+$a[3] < $b[5]*366+$b[4]*31+$b[3]) {
      $hh -= 24;
    }
    if ($hh < 0 && $mm > 0) {
      $hh++;
      $mm = 60-$mm;
    }
    printf "%+03d:%02d\n", $hh, $mm;

Someone pointing out that this is already implemented in a module somewhere in 5, 4, 3, ...

mob
  • 117,087
  • 18
  • 149
  • 283
  • Can you explain the calculation in the if statment -- why multiply the year by the number of days in a leap year? why Month * 31? Why add the day? Thanks. – Kevin Friedheim Sep 14 '10 at 18:21
  • @Kevin Friedman - it is a test to see if `@a[5,4,3]` and `@b[5,4,3]` represent the same day. I combine year, month, and day into a single number so I can get by with one comparison instead of three. You could use larger numbers than 366 and 31 and you would still get the right result. – mob Sep 14 '10 at 20:05
  • You should use bit fields instead of multiplication if you want to be fast: `($a[5]<<13)|($a[4]<<5)|$a[3]`. Also, the `<=>` operator that could be used to replace the if statement. – dolmen Jun 21 '11 at 12:03
  • Even faster: `($a[5]<<9)|$a[7]` (`$a[7]` is the day number in the year) – dolmen Jun 21 '11 at 12:53