Convert filetime to GMT in Perl - perl

I am working on some training material for new programmers where I am talking about the HTTP Header, so I am trying to set the Last-Modified manually. I have everything worked out except for getting the file time to GMT. Below is what I have so far:
The question is: giving stat($fh)->mtime which could be running in any timezone, what code needs to be added to convert to GMT?
my $scriptFilename = $ENV{'SCRIPT_FILENAME'};
my $timestamp;
my $fh = FileHandle->new;
if ($fh->open("< ${scriptFilename}")) {
$timestamp = time2str("%a, %e %b %Y %X %Z", stat($fh)->mtime);
$fh->close;
}
#Last-Modified: Tue, 15 Oct 2019 12:45:26 GMT
print <<"END";
Content-type: text/html; charset=iso-8859-1
Last-Modified: $timestamp
<html>
...
</html>

time2str from the Date::Format package (I assume that's where time2str comes from) takes an optional 3rd argument to specify the timezone.
$timestamp = time2str("%a, %e %b %Y %X %Z", stat($fh)->mtime, 'UTC');

There seems to be some confusion here: the result of mtime is not in any time zone, it is in seconds since the epoch which is a fixed point in time (barring leap seconds which are ignored). Thus all you need to do is represent it in the time zone you want, which is UTC. Another answer mentioned how to do this with the function you had been using, but the usual function to format a time into a string is strftime, which is provided by a couple core modules.
use strict;
use warnings;
use POSIX 'strftime';
# gmtime interprets the mtime seconds in UTC
my $timestamp = strftime "%a, %e %b %Y %X %Z", gmtime $mtime;
use Time::Piece 'gmtime';
my $timestamp = gmtime($mtime)->strftime("%a, %e %b %Y %X %Z");
But your use case is actually a specific date format, the one the HTTP protocol uses, and which will always be in GMT/UTC. There is a module for that: HTTP::Date
use strict;
use warnings;
use HTTP::Date 'time2str';
my $timestamp = time2str $mtime;

Basically
my $dt = DateTime->new(
year => 2000,
month => 5,
day => 10,
hour => 15,
minute => 15,
time_zone => 'America/Los_Angeles',
);
print $dt->hour; # prints 15
$dt->set_time_zone( 'America/Chicago' );
print $dt->hour; # prints 17
See here for how to format the output.

Related

Perl converting formatting date to y-m-d H:M

I am running into issues converting the date Apr 9 2017 3:45:00:000AM to 2017-04-09 03:45:00
Here's what i have tried.
use DateTime::Format::Strptime;
my $strp = DateTime::Format::Strptime->new(pattern => '%h %d %Y %H:%M',);
$start_date = $strp->parse_datetime('Apr 9 2017 3:45:00:000AM');
prints 2017-04-09T03:45:00 and not 2017-04-09 03:45:00. Trying to get 24 hour clock so when i switch AM to PM the same time prints.
Update   See end for an strptime pattern to parse the shown input string format correctly
What is shown works,† and so the DateTime::Format::Strptime constructor returns a DateTime object. But when an object is simply printed then one gets the stringification that is shown.
So you need to format it for printing as desired. A general way is with strftime
say $start_date->strftime("%F %T");
where both %F and %T are shorthands, see strftime patterns
Or combine the basic ymd and hms methods
say $start_date->ymd('-') . ' ' . $start_date->hms(':');
See the docs and adjust if/as needed. I didn't understand some details.
† It works only by accident in this exact example, since the pattern given to use for parsing in new is wrong for the shown input format, and is also inconsistent with stated requirements
The shown pattern doesn't have a format specifier for the seconds, nor for the milliseconds that follow it, nor for the following AM/PM -- all expected in the input string. So in general an input in the shown form cannot be parsed correctly with the shown pattern
The %H matches 00-23 hour, so not 12-hour clock which is stated as expected and is implicit by the presence of AM. (It still matches a 12-hour-time number but it won't once the missing AM/PM format specifier is added.)
The pattern in the OP works and parses the given input correctly because 03:45... happens to be in AM, and the module uses regex to match the given pattern anywhere in the given string (by default), so %H:%M matches 03:45 and the rest of the input string doesn't matter. If we turn on strict in the constructor this'll fail. See documentation.
Assuming that the shown input is the correct part we'd need
my $strp = DateTime::Format::Strptime->new(
pattern => '%h %d %Y %I:%M:%S%3N%p'
);
Here %I is for 12-hour clock (1-12), added %S is for seconds and %3N for milliseconds (see the page in docs for patterns, linked above), and %p for AM/PM.
The rest then works as it stands, along with printing in a desired format given above.
Following code demonstrates how desired output can be achieved without any modules
use strict;
use warnings;
use feature 'say';
my $date = 'Apr 9 2017 3:45:00:000AM';
my #fields = qw/month mday year hour min sec usec/;
my %months = ( Jan => 1, Feb => 2, Mar => 3,
Apr => 4, May => 5, Jun => 6,
Jul => 7, Aug => 8, Sep => 9,
Oct => 10, Nov => 11, Dec => 12
);
my %parts;
#parts{#fields} = split "[ :]+", $date;
$parts{hour} += 12 if $parts{usec} =~ /PM/;
$parts{month} = $months{ $parts{month} };
printf "%04d-%02d-%02d %02d:%02d:%02d\n",
#parts{qw/year month mday hour min sec/};
Perl code with assistance of module
use strict;
use warnings;
use feature 'say';
use DateTime::Format::DateParse;
my($date, $dt);
$date = 'Apr 9 2017 3:45:00:000AM';
$date =~ s/:(\d{3}[AP]M)/.$1/;
$dt = DateTime::Format::DateParse->parse_datetime( $date );
$date = $dt;
$date =~ s/T/ /;
say $date;
There are two issues with your code. First, the pattern you use to parse your date is not correct: %H is used for a 24-hour format hour. Instead, you should use a combination of %i (12-hour) and %p (AM/PM). Second, to print a DateTime object, you should first format it first (using ->strftime or ->ymd() for instance).
The milliseconds in the date are, however, a bit of an issue because strptime does not have a option to match milliseconds. I suggest to first remove them from your date, and only then parse the date with strptime:
use DateTime::Format::Strptime;
my $date = 'Apr 9 2017 3:45:00:505PM';
# Removing milliseconds from date
$date =~ s/:(\d{3})(?=AM|PM)//;
my $milliseconds = $1;
my $strp = DateTime::Format::Strptime->new(pattern => '%h %d %Y %I:%M:%S%p',);
my $start_date = $strp->parse_datetime($date);
# Taking into account the milliseconds that were removed earlier
$start_date->add(seconds => 1) if $milliseconds > 500;
say $start_date->strftime("%F %T");

How to format date to "2018-10-29 11:49:33"

I have to convert the GMT date to region specific date with format like "YYYY-MM-DD H:M:S".
Code developed is :-
use Time::Local;
($year,$mon,$day) = split /\-/, $ARGV[0];
($hrs,$min,$sec ) = split /:/, $ARGV[1];
$time = timegm( $sec, $min, $hrs, $day, $mon-1, $year-1900);
print scalar localtime($time), "\n";
But when I run it like :-
$ perl testDateGMTToLocal.pl 2018-10-29 11:49:33
It gives o/p converted in local time zone:-
Mon Oct 29 07:49:33 2018
But I want this o/p in below format
29-OCT-18 07:49:33
Thanks in advance.
I'd recommend to do it all using modules. The all-capable and very complete module is DateTime, and for this job you'd also need DateTime::Format::Strptime.
One other option is the simpler and much smaller core module Time::Piece
use warnings;
use strict;
use feature 'say';
use Time::Piece;
die "Usage $0 YYYY-MM-DD H:M:S" if #ARGV != 2;
my $time = join ' ', #ARGV;
my $tp = Time::Piece->strptime($time, "%Y-%m-%d %T");
my $local = localtime($tp->epoch);
say $local;
# In the desired format
say join('-', $local->mday, uc $local->month, $local->yy),
' ', $local->hms;
# If "Oct" is ok instead of block capitals for month abbreviation
say $local->strftime("%d-%b-%y %T");
This converts GMT time, with invocation as in the question, to the local time on my machine
Mon Oct 29 04:09:33 2018
29-OCT-18 04:09:33
29-Oct-18 04:09:33
where the middle one was asked for.
On some systems there is the %F format specifier for %Y-%m-$d.† There may be a defined format for 29-OCT-18, in which case you don't have to patch it by hand, but I am not aware of it.
† Or the module has its own formatting in which case that's portable. But origin of the error when it fails to do %F on my system isn't clear to me in that sense.
You can use
use POSIX qw( strftime );
print(strftime("%d-%b-%y %H:%M:%S", localtime($time)), "\n");

To subtract two Time:Piece objects

I want to get the difference between time in minutes. Both the timings to be compared are in same timezone so no worries about the time zone difference and all.
Say if Start_time = 14 Apr 2016 05:02:26 (which is collected form log line) and for end time would be the current time of machine,
I want to calculate end time - start time. For that I need the Start time to be in format of current time.
I tried subtracting current time which is already a Time::Piece Object and converting $start_time into Time:Piece object.
But I am getting "Error parsing time at C:/Perl/lib/Time/Piece.pm line 469, line 1071883." error. Please suggest.
Also this "1071883" num in error is changing everytime I run the script. Not sure if its a kind of garbage value or what.
Editing the code with below suggested answer
I am getting below output. Seems the problem is coming as $now contains the DAY value like saturday but our $start_time doesnt. However i cant make any change in start time like adding the day value to it as we are collecting it from a log file. If the problem is due to the reason I stated, Kindly suggest how to ignore that day value from $now.
Output :
last line of log: 16 Apr 2016 03:41:49 -- DEBUG -- 16 Apr 2016 03:41:49
Time is Sat Apr 16 03:43:02 2016
difference is 21673
Below is what I tried:
#get last line of log
open my $fh ,"<","$slogfile";
my $last_line;
$last_line = $_,while (<$fh>);
print OUTLOG "last line of log: $last_line \n";
if ($last_line=~ m/^(\d\d) (\w{3}) (\d{4}) (\d\d):(\d\d):(\d\d) --/) {
$start_time = "$1 $2 $3 $4:$5:$6";
print OUTLOG "$start_time\n";
} else {
print OUTLOG "pattern matching didnt work\n";
}
#get current time
my $t = localtime;
#my $current_time = $t ;
print OUTLOG "Time is $current_time \n";
my $format = '%d %b %Y %H:%M:%S';
my $diff = $t - Time::Piece->strptime($var, $format);
print OUTLOG "difference is $diff \n";
Your format doesn't match the format of your date. The format you're using is "'%a %b %d %H:%M:%S %Y", which would match "Thu Apr 14 05:02:26 2016". You can get an explanation of the various pieces of your format from "man strftime" or "man strptime".
I've corrected the format definition in this example.
#!/usr/bin/perl
use strict;
use warnings;
use 5.010;
use Time::Piece;
my $format = '%d %b %Y %H:%M:%S';
my $start_time = '14 Apr 2016 05:02:26';
my $now = localtime;
my $diff = $now - Time::Piece->strptime($start_time, $format);
say "$diff seconds"; # $diff stringifies to seconds
say $diff->minutes, ' minutes';
The output is:
186402 seconds
3106.7 minutes
Update:
Seems the problem is coming as $now contains the DAY value like
saturday but our $start_time doesnt. However i cant make any change in
start time like adding the day value to it as we are collecting it
from a log file. If the problem is due to the reason I stated, Kindly
suggest how to ignore that day value from $now.
No. That's not the problem at all. If you print out the the Time::Piece object that you parse from $start_time then you'll see that also has the day name included. That's just how Time::Piece objects stringify.
The actual problem is more subtle. It seems that when Time::Piece parses a date string, it assumes that it is in UTC unless the string contains an explicit time zone. From what you're saying I'm assuming that your on the east coast of the US, which would explain the ~6 hour differences that you're getting.
I'm investigating this further and will almost certainly submit a patch to Time::Piece to fix this (it might just be a documentation patch to make the behaviour clearer). But in the meantime, you need a fix. And that's pretty simple. You just need to add the time zone to your code. The relevant sections of your code will now look like this:
my $format = '%d %b %Y %H:%M:%S%z'; # %z added here
# Append timezone here (-0600 as you're six hours behind UTC -
# adjust that if my assumption is wrong)
my $diff = $t - Time::Piece->strptime($var . '-0600', $format);
Or (as, quite rightly, pointed out in the comments by Borodin) you could just switch to using UTC throughout).
my $t = gmtime;
my $format = '%d %b %Y %H:%M:%S';
my $diff = $t - Time::Piece->strptime($var, $format);

Perl code to change the date format from IST/UTC to YYYY-MM-DD format

Input:
$str="Thu Mar 25 01:48:45 IST 2011";
Desired output:
2011-03-25
I want only date, not the time.
#!/usr/bin/env perl
use strict;
use warnings;
use Time::Piece;
my $tstamp = Time::Piece->strptime
("Thu Mar 25 01:48:45 2011", "%a %b %d %H:%M:%S %Y");
print $tstamp->strftime("%Y-%m-%d\n");
use Date::Manip;
$str =~ s/[[:upper:]]{3}//; # Remove timezone
$d = ParseDate($str);
die "Invalid date\n" unless $d;
$d=~s/(....)(..)(..).*/$1-$2-$3/;
print "$d\n";
Heck, if you know the format of the date, you don't even need to use a Perl module to manipulate the date and time:
my %months = (Jan => 1, Feb => 2, Mar => 3, Apr => 4 ...);
my $st r= "Thu Mar 25 01:48:45 IST 2011";
$st =~! /\S+\s+(\S+)\s+(\S+)\s+\S+\s+\S+(\S+)/;
my $date = sprintf "%s-%02s-%02s", $3, $months{$1}, $2;
Okay, this is very error prone, and you probably want to do a lot of error checking. The regular expression I used could be formatted a bit stronger (checking for characters and numbers instead of just "not white space". And, you probably want to make sure the month is valid too.
Actually, you're better off using a Date/Time module to do this. I was going to recommend Time::Piece, but James_R_Ferguson beat me to it.

How can I convert of the unix date output across multiple time zones to UTC, in Perl?

In Perl, how would one efficiently parse the output of unix's date command, taking into account time zone, and also convert to UTC?
I've read many similar questions on stackoverflow, but few seem to take into account parsing multiple time zones. Instead they seem to set the timezone manually and assume it to stay fixed.
# Example Input Strings:
my #inputs = (
'Tue Oct 12 06:31:48 EDT 2010',
'Tue Oct 12 07:49:54 BST 2010',
);
I tried the following to no avail:
foreach my $input ( #inputs ) {
my $t = Time::Piece->strptime( $input,
'%a %b %d %T %Z %Y' );
print $t->cdate, "\n";
}
It seems the problem is the time zone (%Z). Additionally, a time zone field does not seem to exist in Time::Piece, which would require me to write custom code to convert to UTC, which just seems... wrong.
Context:
I'm attempting to parse legacy logs from a variety of sources that use the unix date command for timestamps. Ideally, I'd like to convert all timestamps to UTC.
Any help would be greatly appreciated.
If you know how to disambiguate the TZs, just pop them into a dispatch table:
use strict; use warnings;
use DateTime::Format::Strptime ();
my #inputs = (
'Tue Oct 12 06:31:48 EDT 2010',
'Tue Oct 12 07:49:54 BST 2010',
);
my %tz_dispatch = (
EDT => build_parser( 'EST5EDT' ),
BST => build_parser( '+0100' ),
# ... etc
default => build_parser( ),
);
for my $input (#inputs) {
my ($parser, $date) = parse_tz( $input, %tz_dispatch );
print $parser->parse_datetime( $date ), "\n";
}
sub build_parser {
my ($tz) = #_;
my %conf = (
pattern => '%a %b %d %T %Z %Y',
on_error => 'croak',
);
#conf{qw/time_zone pattern/} = ($tz, '%a %b %d %T %Y')
if $tz;
return DateTime::Format::Strptime->new( %conf );
}
sub parse_tz {
my ($date, %tz_dispatch) = #_;
my (#date) = split /\s/, $date;
my $parser = $tz_dispatch{splice #date, 4, 1};
return $parser
? ($parser, join ' ', #date)
: ($tz_dispatch{default}, $date);
}
The Perl DateTime FAQ on timezones has a good background on why EDT and EST cannot be used in most conversions. The issue is that other countries also have an Eastern time zone with the same 3 letter abbreviation. EST EDT is ambiguous without other clues.
You might look at other modules, or just assume that "EDT" is the same as "EST5EDT" if that is true.
If you are using Date::Time::Strptime, you can use %O for the Olson Time Zone name and do a manual fixup before parsing.
i.e. if you know that EDT in your input means America/New_York, do this:
$time_in =~ s{EDT}{America/New_York};
and instead of
%a %b %d %T %Z %Y
for your time zone spec use
%a %b %d %T %O %Y
I've always found Date::Manip::ParseDate to be good for these sorts of situations.
use strict;
use warnings qw<FATAL all>;
use Date::Manip qw<ParseDate UnixDate>;
my #inputs = (
q<Tue Oct 12 06:31:48 EDT 2010>,
q<Tue Oct 12 07:49:54 BST 2010>,
);
sub date2epoch($) {
my $user_string = shift();
my $timestamp = ParseDate($user_string);
my $seconds = UnixDate($timestamp, "%s");
return $seconds;
}
sub epoch2utc($) {
my $seconds = shift();
return gmtime($seconds) . q< UTC>;
}
for my $random_date (#inputs) {
my $epoch_seconds = date2epoch($random_date);
my $normal_date = epoch2utc($epoch_seconds);
print "$random_date == $normal_date\n";
}
When run, that produces this:
Tue Oct 12 06:31:48 EDT 2010 == Tue Oct 12 10:31:48 2010 UTC
Tue Oct 12 07:49:54 BST 2010 == Tue Oct 12 06:49:54 2010 UTC
which seem to be what you're looking for.
I'm a little late on this, but GNU date itself is good at parsing dates:
$ date -u -d 'Thu Oct 14 01:17:00 EDT 2010'
Thu Oct 14 05:17:00 UTC 2010
I don't know how it resolves the EDT ambiguity though.
I agree with Jander on date command. -d and -u are great and save a lot of code lines.