How I can I convert timezones in Perl? - perl

I am trying to convert a date/time GMT 0 to GMT -6 in Perl.
For example, a DHCP Server lease time is in the following format:
2010/02/18 23:48:37
I am trying to convert that time to the Localtime zone (GMT -6) but need it to honor Daylight savings time.
The script below may be overkill, but I am not sure how to proceed from here. (Any suggestions would be awsome).
my $TIMESTART;
$TIMESTART = "2010/02/18 23:48:37";
$TIMESTART =~ s/\//-/g;
use DateTime;
use DateTime::TimeZone;
use DateTime::Format::MySQL;
my $dt = DateTime::Format::MySQL->parse_datetime($TIMESTART);
my $tz = DateTime::TimeZone->new( name => 'America/Chicago' );
print $tz->offset_for_datetime($dt) . "\n";
It will output the following lines:
2010-02-18T23:48:37
-21600
I need to be able to add -21600 to the date to get the local time zone of GMT -6 but I am not sure how to approch this.

Call set_time_zone method 2 times:
my $dt = DateTime::Format::MySQL->parse_datetime($TIMESTART);
$dt->set_time_zone('UTC'); ## set timezone of parsed date time
$dt->set_time_zone('America/Chicago'); ## change timezone in safe way
print DateTime::Format::MySQL->format_datetime($dt),"\n"; ## check the result
How it works:
when you create DateTime object without time zone specified, "floating" time zone is set
first call of set_time_zone change time zone to UTC without conversion
second call of set_time_zone change UTC to America/Chicago

This will convert UTC time into ETC time.
You can also use date/time in any format using +FORMAT parameter of date.
date --date='TZ="ETC" 18:30'

Time::Piece is a very lightweight piece of code of good quality. Or you can just use built-ins and strftime and POSIX::strptime

Don't Use Timezones With `DateTime::Format::MySQL->parse_datetime`
There is a very good reason you shouldn't be using this when doing anything relating to timezones. Why? Because it has absolutely no support for timezones whatsoever. Please see the documentation:
All of the parsing methods set the returned DateTime object's time zone to the floating time zone, because MySQL does not provide time zone information. (Source: MetaCPAN: DateTime::Format::MySQL.)
If you DO use this, it will assume your system time zone default. This might not be the timezone you want to convert from! (Although this will work for many people, it only works as long as your servers don't change their default timezone — after that, everything breaks.)
Use `DateTime->new()`, Because It DOES Support TimeZones
Use the constructor that DateTime provides directly (Source: MetaCPAN: DateTime.):
my $dt = DateTime->new(
year => 1966,
month => 10,
day => 25,
hour => 7,
minute => 15,
second => 47,
nanosecond => 500000000,
time_zone => 'America/Chicago',
);
Convert TimeZones with DateTime
Your starting time zone is whatever you feed into the constructor (i.e., America/Chicago in the above example). To convert a time to an ending time zone, use set_time_zone().
Working Code
In the code below, a time is converted from one timezone to another, and it does this perfectly every time, even you are converting New York time to San Francisco time with a server whose Linux OS is in Singapore time.
use strict;
use DateTime;
sub convertTimeZonesForTime {
my ($args) = #_;
my $time = $args->{time};
my $date = $args->{date};
my $totimezone = $args->{totimezone};
my $fromtimezone = $args->{fromtimezone};
my $format = $args->{format} || '%H:%M:%S';
my ($year, $month, $day) = map {int $_} split('-', $date);
my ($hour, $minute, $second) = map {int $_} split(':', $time);
$year ||= 1999 if !defined $year;
$month ||= 1 if !defined $month;
$day ||= 1 if !defined $day;
$hour ||= 12 if !defined $hour;
$minute ||= 30 if !defined $minute;
$second ||= 0 if !defined $second;
my $dt = DateTime->new(
year=>$year,
month=>$month,
day=>$day,
hour=>$hour,
minute=>$minute,
second=>$second,
time_zone => $fromtimezone,
);
my $formatter = new DateTime::Format::Strptime(pattern => $format);
$dt->set_formatter($formatter);
$dt->set_time_zone($totimezone);
return "$dt";
}
print(convertTimeZonesForTime({
'totimezone'=>'America/Denver',
'fromtimezone'=>'US/Eastern',
'time'=>'12:30:00',
}));
Output:
10:30:00

#!/usr/bin/perl -w
($sec,$min,$hour,$mday,$mon,$year) = gmtime(time+21600);
$year = $year + 1900;
printf("%02d:%02d:%02d %02d.%02d.%04d\n", $hour,$min,$sec,$mday,$mon,$year);

Related

Subtract two date strings in Perl with conversion to unix time and reverting back

I want to subtract two timestamps in Perl. I converted them to unix-time via the function below and convert the unix timestamp back to how it was. In the example below the result is 01:20:00 instead of 00:20:00
(I think it has sth to do with the start of the unix timestamp 1.1.1970 01:00:00 but not sure how to resolve it)
Any idea? Many thanks for your help in advance.
use POSIX qw( strftime );
use Time::Local qw( timelocal );
sub to_epoch {
$_ = shift;
my #a = split /\W+/, $_;
my $b = timelocal($a[5],$a[4],$a[3],$a[2],$a[1],$a[0]);
return $b;
}
my $h_end = "2018.11.12 00:50:00";
my $h_start = "2018.11.12 00:30:00";
my $duration = to_epoch($h_end) - to_epoch($h_start);
my $convert_back = POSIX::strftime("%H:%M:%S", localtime($duration));
print $convert_back , "\n";
Ouptut: 01:20:00
It works for me. But I think that's because I'm in GMT and you're in CET (GMT+1).
The flaw is in your final step. You are confusing two concepts - a point in time and a duration.
You correctly convert your two points in time to Unix epoch numbers and then you subtract those numbers to get the number of seconds between them. That number is a duration. And you want to convert that duration into a human-readable format. Using localtime() and POSIX::strtime() is not the way to do that. POSIX::strftime() and localtime() deal with points in time, not durations.
The number you get is 1,200. By passing that to localtime() you are saying "what is the epoch number 1,200 when converted to a date and time in my local timezone?" 1,200 is 20 past midnight on Jan 1st 1970 GMT. But in your local, Frankfurt, timezone, it's 20 past 1am. Which is why you're getting 1:20 and I'm getting 0:20.
There are a couple of ways to fix this. You can do the conversion manually.
my $duration = 1_200;
my $mins = int($duration/60);
my $secs = $duration % 60;
Or you can use a proper date/time handling module like DateTime (along with its associated module DateTime::Duration).
It might work if you use timegm() and gmtime() in place of timelocal() and localtime() - but I really don't recommend this approach as it perpetuates the confusion between points in time and durations.
Update: A version using DateTime.
#/usr/bin/perl
use strict;
use warnings;
use DateTime;
use DateTime::Format::Strptime;
my $h_end = '2018.11.12 00:50:00';
my $h_start = '2018.11.12 00:30:00';
my $date_p = DateTime::Format::Strptime->new(
pattern => '%Y.%m.%d %H:%M:%S'
);
my $duration = $date_p->parse_datetime($h_end)
- $date_p->parse_datetime($h_start);
printf '%02d:%02d:%02d', $duration->in_units('hours', 'minutes', 'seconds');
1200, the value of $duration, signifies the following when treated as a epoch timestamp
1970-01-01T01:20:00+01:00
^^^^^^^^
The solution is to replace
strftime("%H:%M:%S", localtime($duration));
with
strftime("%H:%M:%S", gmtime($duration));
This gives
1970-01-01T00:20:00Z
^^^^^^^^
Of course, this is still a hack. You're not suppose to be passing a duration to gmtime. Use an appropriate module instead.
use DateTime::Format::Strptime qw( );
my $format = DateTime::Format::Strptime->new(
pattern => '%Y.%m.%d %H:%M:%S',
on_error => 'croak',
);
my $h_end = $format->parse_datetime('2018.11.12 00:50:00');
my $h_start = $format->parse_datetime('2018.11.12 00:30:00');
my $dur = $h_end - $h_start;
printf "%02d:%02d:%02d\n", $dur->in_units(qw( hours minutes seconds ));
By the way,
timelocal($a[5],$a[4],$a[3],$a[2],$a[1],$a[0])
should be
timelocal($a[5],$a[4],$a[3],$a[2],$a[1]-1,$a[0])

Compare date time zone with time() in perl

I am trying to compare a file creation time which is in the format: 08-07-2016 08:16:26 GMT with the current time using time() in perl.
Since time() returns epoch time, I am not sure how to find the time difference between these two different time formats.
I tried something like below and for obvious reasons, I get an error saying: "Argument 08-07-2016 08:16:26 GMT" isn't numeric in subtraction".
my $current_time = time();
my $time_diff = $creation_time - $current_time;
if ($time_diff > 10) { #compare if the difference is greater than 10hours
# do something...
}
Some of the questions I have:
Since I want to compare only the hour difference, how can I extract just the hours from both these time formats?
I am unsure if the comparison of $time_diff > 10 is right. How to represent 10hours? 10*60?
OR is there a way to at least convert any given time format into epoch using DateTime or Time::Local?
How can I pass a a date parameter to a DateTime constructor?
my $dt1 = DateTime-> new (
year =>'1998',
month =>'4',
day =>'4',
hour =>'21',
time_zone =>'local'
);
Instead can we do something like
my $date = '08-07-2016 08:16:26 GMT';
my $dt1 = DateTime->new($date); # how can i pass a parameter to the constructor
print Dumper($dt1->epoch);
Thanks in advance for any help.
Time::Piece has been a standard part of Perl since 2007.
#!/usr/bin/perl
use strict;
use warnings;
use 5.010;
use Time::Piece;
use Time::Seconds;
my $creation_string = '08-07-2016 08:16:26 GMT';
my $creation_time = Time::Piece->strptime($creation_string, '%d-%m-%Y %H:%M:%S %Z');
my $current_time = gmtime;
my $diff = $current_time - $creation_time;
say $diff; # Difference in seconds
say $diff->pretty;

Create time variable in perl

I want to create a variable that will store hardcoded hours and minutes value. Rest stuff like min, day, date, year should be current date and time. How can I do this? I tried timelocal, localtime and many others, but they didn't work.
If you are interested in a complete date time object (like it seems according your description), use DateTime. Example:
use DateTime;
my $dt = DateTime->now; # initialize with current date time
$dt->set_hour(20); #set your own hour
$dt->set_minute(20); #set your own minute
Unless I'm missing something, you indeed use localtime, as in perldoc:
http://perldoc.perl.org/functions/localtime.html
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
Then, if you want to stick to using the broken-down components, you could overwrite whichever variables you want to "fix":
$hour = $my_hardcoded_hour;
$min = $my_hardcoded_minute;
If instead you need to translate this back to "seconds since the epoch" (Jan 1, 1970, 12:00AM GMT) format, you need the Time::Local package:
http://perldoc.perl.org/Time/Local.html
use Time::Local;
$epoch_current_time_with_fixed_hour_min =
timelocal($sec, $my_hardcoded_minutes, $my_hardcoded_hour, $mday, $mon, $year);

How to calculate local time at timezone given GMT/UTC offset in Perl?

I need to find out what is local time at a given location. I have GMT/UTC offset for that location. I am trying to get a time duration by taking a difference between deadline set in that time zone to trigger emails being sent out when deadline is met in that perticular time zone.
Ex.If deadline is set in Seattle to be Sept 10, 2011 12:00:00 GMT -7:00 now if I am in UK I need to calculate what time is now in Seattle given GMT offset -7:00 once I get that I can calculate the difference if the difference is 0 then I will sent out an email saying deadline is met.
How can I do the time calculation part in Perl?
Please help.
Thanks,
Sunyl
Create a DateTime object, and compare it to DateTime->now. The DateTime object is aware of the time zone associated with the timestamp therein, so it can do what you want with no fuss.
use strict;
use warnings;
use feature qw( say );
use DateTime qw( );
use DateTime::Format::Strptime qw( );
my $strp = DateTime::Format::Strptime->new(
pattern => '%b %d, %Y %H:%M:%S GMT%z',
locale => 'en',
on_error => 'croak',
);
my $target = 'Sep 10, 2011 12:00:00 GMT-0700';
my $target_dt = $strp->parse_datetime($target);
my $now_dt = DateTime->now();
if ($now_dt > $target_dt) {
say "It's too late";
} else {
say "It's not too late";
}
$target_dt->set_time_zone('local');
say "The deadline is $target_dt, local time";
Above, I assumed you miscopied the date format. If the date is formatted as you provided, you won't be able to use Strptime because the timestamp uses nonstandard names for the months and a nonstandard format for the offset.
my #months = qw( ... Sept ... );
my %months = map { $months[$_] => $_+1 } 0..$#months;
my ($m,$d,$Y,$H,$M,$S,$offS,$offH,$offM) = $target =~
/^(\w+) (\d+), (\d+) (\d+):(\d+):(\d+) GMT ([+-])(\d+):(\d+)\z/
or die;
my $target_dt = DateTime->new(
year => $Y,
month => $months{$m},
day => 0+$d,
hour => 0+$H,
minute => 0+$M,
second => 0+$S,
time_zone => sprintf("%s%04d", $offS, $offH * 100 + $offM),
);
DateTime
DateTime::Format::Strptime
You can use the DateTime module from CPAN to do time calculations.
http://metacpan.org/pod/DateTime
It's got time zone stuff that you can leverage as well. Should be pretty straight forward as the documentation is pretty clear.
Specifically,
$dt->subtract_datetime( $datetime )
This method returns a new DateTime::Duration object representing the difference between the two dates. The duration is relative to the object from which $datetime is subtracted. For example:
2003-03-15 00:00:00.00000000
- 2003-02-15 00:00:00.00000000
-------------------------------
= 1 month
Note that this duration is not an absolute measure of the amount of time between the two datetimes, because the length of a month varies, as well as due to the presence of leap seconds.
Hope that helps!
Edit:
Also this is probably important/will make life easier,
use UTC for all calculations
If you do care about time zones (particularly DST) or leap seconds, try to use non-UTC time zones for presentation and user input only. Convert to UTC immediately and convert back to the local time zone for presentation:
my $dt = DateTime->new( %user_input, time_zone => $user_tz );
$dt->set_time_zone('UTC');
# do various operations - store it, retrieve it, add, subtract, etc.
$dt->set_time_zone($user_tz);
print $dt->datetime;

How can I calculate the number of days between two dates in Perl?

I want to calculate (using the default Perl installation only) the number of days between two dates. The format of both the dates are like so 04-MAY-09. (DD-MMM-YY)
I couldn't find any tutorials that discussed that date format. Should I be building a custom date checker for this format? Further reading of the Date::Calc on CPAN it looks unlikely that this format is supported.
There seems to be quite a bit of confusion because, depending on what you are trying to accomplish, “the number of days between two dates” can mean at least two different things:
The calendar distance between the two dates.
The absolute distance between the two dates.
As an example and to note the difference, assume that you have two DateTime objects constructed as follows:
use DateTime;
sub iso8601_date {
die unless $_[0] =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/;
return DateTime->new(year => $1, month => $2, day => $3,
hour => $4, minute => $5, second => $6, time_zone => 'UTC');
}
my $dt1 = iso8601_date('2014-11-04T23:35:42Z');
my $dt2 = iso8601_date('2014-11-07T01:15:18Z');
Note that $dt1 is quite late on a Tuesday, while $dt2 is very early on the following Friday.
If you want the calendar distance use:
my $days = $dt2->delta_days($dt1)->delta_days();
print "$days\n" # -> 3
Indeed, between, Tuesday and Friday there are 3 days. A calendar distance of 1 means “tomorrow” and a distance of -1 means “yesterday”. The “time” part of the DateTime objects is mostly irrelevant (except perhaps if the two dates fall on different time zones, then you would have to decide what “the calendar distance” between those two dates should mean).
If you want the absolute distance then instead use:
my $days = $dt2->subtract_datetime_absolute($dt1)->delta_seconds / (24*60*60);
print "$days\n"; # -> 2.06916666666667
Indeed, if you want to split the time between the two dates in 24-hour chunks, there are only about 2.07 days between them. Depending on your application, you might want to truncate or round this number. The “time” part of the DateTime objects is very relevant, and the expected result is well defined even for dates on different time zones.
If you care about accuracy, keep in mind that not all days have 86400 seconds. Any solution based on that assumption will not be correct for some cases.
Here's a snippet I keep around to calculate and display date/time differences a few different ways using the DateTime library. The last answer printed is the one you want, I think.
#!/usr/bin/perl -w
use strict;
use DateTime;
use DateTime::Format::Duration;
# XXX: Create your two dates here
my $d1 = DateTime->new(...);
my $d2 = DateTime->new(...);
my $dur = ($d1 > $d2 ? ($d1->subtract_datetime_absolute($d2)) :
($d2->subtract_datetime_absolute($d1)));
my $f = DateTime::Format::Duration->new(pattern =>
'%Y years, %m months, %e days, %H hours, %M minutes, %S seconds');
print $f->format_duration($dur), "\n";
$dur = $d1->delta_md($d2);
my $dy = int($dur->delta_months / 12);
my $dm = $dur->delta_months % 12;
print "$dy years $dm months ", $dur->delta_days, " days\n";
print $dur->delta_months, " months ", $dur->delta_days, " days\n";
print $d1->delta_days($d2)->delta_days, " days\n";
Time::ParseDate will handle that format just fine:
use Time::ParseDate qw(parsedate);
$d1="04-MAR-09";
$d2="06-MAR-09";
printf "%d days difference\n", (parsedate($d2) - parsedate($d1)) / (60 * 60 * 24);
Date::Calc has Decode_Date_EU (and US etc)
#!/usr/bin/perl
use Date::Calc qw(Delta_Days Decode_Date_EU);
($year1,$month1,$day1) = Decode_Date_EU('02-MAY-09');
($year2,$month2,$day2) = Decode_Date_EU('04-MAY-09');
print "Diff = " . Delta_Days($year1,$month1,$day1, $year2,$month2,$day2);
This question already has a nice answer, but I want to provide a answer showing why calculating the difference in seconds is WRONG (when we're using formatted/local dates rather than floating dates).
I find it distressing how many suggestions tell people to subtract seconds. (This question was the first Google hit for my search, so I don't care how old it is.)
I've made that mistake myself and wondered why the application would suddenly (over the weekend) show incorrent times. So I'm hoping this code will help people (who may be facing such an issue) understand why this approach is wrong and help them avoid that mistake.
Here is a complete example, one that doesn't contain "..." at some crucial point (because if you insert two dates in the same time zone, you may not see an error).
#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;
use DateTime;
# Friday, Oct 31
my $dt1 = DateTime->new(
time_zone => "America/Chicago",
year => 2014,
month => 10,
day => 31,
);
my $date1 = $dt1->strftime("%Y-%m-%d (%Z %z)");
# Monday, Nov 01
my $dt2 = $dt1->clone->set(month => 11, day => 3);
my $date2 = $dt2->strftime("%Y-%m-%d (%Z %z)");
# Friday, Mar 06
my $dt3 = DateTime->new(
time_zone => "America/Chicago",
year => 2015,
month => 3,
day => 6,
);
my $date3 = $dt3->strftime("%Y-%m-%d (%Z %z)");
# Monday, Mar 09
my $dt4 = $dt3->clone->set(day => 9);
my $date4 = $dt4->strftime("%Y-%m-%d (%Z %z)");
# CDT -> CST
print "dt1:\t$dt1 ($date1):\t".$dt1->epoch."\n";
print "dt2:\t$dt2 ($date2):\t".$dt2->epoch."\n";
my $diff1_duration = $dt2->subtract_datetime_absolute($dt1);
my $diff1_seconds = $diff1_duration->seconds;
my $diff1_seconds_days = $diff1_seconds / 86400;
print "diff:\t$diff1_seconds seconds = $diff1_seconds_days days (WRONG)\n";
my $diff1_seconds_days_int = int($diff1_seconds_days);
print "int:\t$diff1_seconds_days_int days (RIGHT in this case)\n";
print "days\t".$dt2->delta_days($dt1)->days." days (RIGHT)\n";
print "\n";
# CST -> CDT
print "dt3:\t$dt3 ($date3):\t".$dt3->epoch."\n";
print "dt4:\t$dt4 ($date4):\t".$dt4->epoch."\n";
my $diff3_duration = $dt4->subtract_datetime_absolute($dt3);
my $diff3_seconds = $diff3_duration->seconds;
my $diff3_seconds_days = $diff3_seconds / 86400;
print "diff:\t$diff3_seconds seconds = $diff3_seconds_days days (WRONG)\n";
my $diff3_seconds_days_int = int($diff3_seconds_days);
print "int:\t$diff3_seconds_days_int days (WRONG!!)\n";
print "days\t".$dt4->delta_days($dt3)->days." days (RIGHT)\n";
print "\n";
Output:
dt1: 2014-10-31T00:00:00 (2014-10-31 (CDT -0500)): 1414731600
dt2: 2014-11-03T00:00:00 (2014-11-03 (CST -0600)): 1414994400
diff: 262800 seconds = 3.04166666666667 days (WRONG)
int: 3 days (RIGHT in this case)
days 3 days (RIGHT)
dt3: 2015-03-06T00:00:00 (2015-03-06 (CST -0600)): 1425621600
dt4: 2015-03-09T00:00:00 (2015-03-09 (CDT -0500)): 1425877200
diff: 255600 seconds = 2.95833333333333 days (WRONG)
int: 2 days (WRONG!!)
days 3 days (RIGHT)
Notes:
Again, I'm using local dates. If you use floating dates, you won't have that problem - simply because your dates stay in the same time zone.
Both time ranges in my example go from friday to monday, so the difference in days is 3, not 3.04... and of course not 2.95...
Turning the float into an integer using int() (as suggested in an answer) is just wrong, as shown in the example.
I do realize that rounding the difference in seconds would also return correct results in my example, but I feel like it's still wrong. You'd be calculating a day difference of 2 (for a large value of 2) and, because it is a large value of 2, turn it into a 3. So as long as DateTime provides the functionality, use DateTime.
Quoting the documentation (delta_days() vs subtract_datetime()):
date vs datetime math
If you only care about the date (calendar) portion of a datetime, you
should use either delta_md() or delta_days(), not subtract_datetime().
This will give predictable, unsurprising results, free from
DST-related complications.
Bottom line: Don't diff seconds if you're using DateTime. If you're not sure what date framework to use, use DateTime, it's awesome.
You could convert the dates into the long integer format, which is the number of seconds since the epoch (some date in 1970 I think). You then have two variables that are the dates in seconds; subtract the smaller from the larger. Now you have a time span in seconds; divide it by the number of seconds in 24 hours.
Convert the two dates to seconds and then do the math:
#!/usr/bin/perl
use strict;
use warnings;
use POSIX qw/mktime/;
{
my %mon = (
JAN => 0,
FEB => 1,
MAR => 2,
APR => 3,
MAY => 4,
JUN => 5,
JUL => 6,
AUG => 7,
SEP => 8,
OCT => 9,
NOV => 10,
DEC => 11,
);
sub date_to_seconds {
my $date = shift;
my ($day, $month, $year) = split /-/, $date;
$month = $mon{$month};
if ($year < 50) { #or whatever your cutoff is
$year += 100; #make it 20??
}
#return midnight on the day in question in
#seconds since the epoch
return mktime 0, 0, 0, $day, $month, $year;
}
}
my $d1 = "04-MAY-99";
my $d2 = "04-MAY-00";
my $s1 = date_to_seconds $d1;
my $s2 = date_to_seconds $d2;
my $days = int(($s2 - $s1)/(24*60*60));
print "there are $days days between $d1 and $d2\n";