Take Longitude/Latitude and get UTC Offset in Perl - perl

I'm trying to localize times that are in UTC when the only thing I know about the destination time is the longitude and latitude. I've come up with something that works, but feels kludgy:
# We need to get localized time for display purposes.
state $moduleGeoLocation = require Geo::Location::TimeZone;
my $gltzobj = Geo::Location::TimeZone->new();
my $tzName = $gltzobj->lookup( lon => $params->{'longitude'}, lat => $params->{'latitude'} );
say "TimeZone: " . $tzName;
So far so good. Here's where the kludge comes in. I'm parsing a time using Time::Piece's strptime but I don't have a GMT offset for the timezone, so after I parse the UTC time in Time::Piece, I'm sending it over to DateTime to do the time zone calculation. It seems rather clunky to be using both DateTime and Time::Piece:
# Get TimeZoneOffset.
state $moduleDateTime = require DateTime;
state $moduleDateTimeTimeZone = require DateTime::TimeZone;
my $timeZone = DateTime::TimeZone->new( 'name' => $tzName );
my $timePiece = Time::Piece->strptime($hour->{'time'}, '%Y-%m-%dT%H:%M:%SZ');
my $time = DateTime->from_epoch( 'epoch' => $timePiece->epoch, 'time_zone' => $timeZone );
Is there a better way to accomplish what I'm doing? I'm aiming for the fastest possible way to get to the result of a localized time.

Your question boils down to the following:
How do I create a DateTime object from a timestamp using the %Y-%m-%dT%H:%M:%S format. I have the appropriate time zone as a DateTime::TimeZone object.
To parse a date-time into a DateTime, one should first look for an appropriate DateTime::Format:: module.
DateTime::Format::Strptime would be the most similar to your current attempt.
use DateTime::Format::Strptime qw( );
my $format = DateTime::Format::Strptime->new(
pattern => '%Y-%m-%dT%H:%M:%S',
strict => 1,
time_zone => $tz,
on_error => 'croak',
);
my $dt = $format->parse_datetime($ts);
You could also use DateTime::Format::ISO8601, although you couldn't use it as a validator since it doesn't accept only the stated format.
use DateTime::Format::ISO8601 qw( );
( my $dt = DateTime::Format::ISO8601->parse_datetime($ts) )
->set_time_zone('floating')
->set_time_zone($tz);
Given that the latter solution overrides any explicitly-provided time zone, I'd use the first solution for clarity.

Related

datetime strptime returning wrong pattern

I pull a date from a file and I'm creating a datetime object by using this command and then adding 1 to it to get the next date.
my $parseit = DateTime::Format::Strptime->new(pattern => '%Y%m%d');
my $lastdate = "20190115";
my $tempdate = $parseit->parse_datetime($lastdate);
my $date_up1 = $tempdate->add(days => 1);
but in printing out the variable $date_up1 I always get it in the form %Y-%m-%d. How can I just get it returned in the pattern that I selected.
strptime and thus DateTime::Format::Strptime by default only dictates how you parse the input into a DateTime object. DateTime objects default to a specific stringification, which you are seeing. In order to stringify it in a certain way, you can use its strftime method.
print $date_up1->strftime('%Y%m%d');
While DateTime::Format::Strptime can be used to both parse and format date times, it doesn't set itself as the default formatter for the DateTime objects it creates as you expect. You can do that explicitly by adding the following:
$tempdate->set_formatter($parseit);
After cleaning up your code, it looks like this:
my $date = "20190115";
my $format = DateTime::Format::Strptime->new(
pattern => '%Y%m%d',
on_error => 'croak',
);
my $dt = $format->parse_datetime($date);
$dt->set_formatter($format);
$dt->add( days => 1 );
say $dt;
Alternatively, all of the following work without setting the formatter:
$format->format_datetime($dt)
$dt->strftime("%Y%m%d") (Most flexible, but introduces duplicate code in this case)
$dt->ymd("") (Simplest in this case)

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])

How can I convert two dates with different formats to DateTime objects so I can compare them?

I need to convert the following strings to DateTime objects so I can compare them:
2016-06-30T09:00:00-04:00
2016-07-01T15:37:25
Both objects should use the EST timezone. How can I do this?
First of all, EST is used to refer to a multitude of different time zone offsets. Presumably, you meant UTC-05:00.
Second of all, if one is to perform datetime arithmetic, one rarely wants to deal with offsets; one almost always wants time zones associated with a geographical location (such as America/New_York). America/New_York would be a suitable choice for use below, but I used the more flexible local instead.
use strict;
use warnings;
use feature qw( say );
use DateTime::Format::Strptime qw( );
my $format1 = DateTime::Format::Strptime->new(
pattern => "%Y-%m-%dT%H:%M:%S%Z",
on_error => "croak",
);
my $format2 = DateTime::Format::Strptime->new(
pattern => "%Y-%m-%dT%H:%M:%S",
time_zone => "local",
on_error => "croak",
);
my $dt1 = $format1->parse_datetime('2016-06-30T09:00:00-04:00');
my $dt2 = $format2->parse_datetime('2016-07-01T15:37:25');
Then, you can do whatever you want. You mentioned you wanted to compare them, which can be done using numerical comparison operators (e.g. $dt1 < $dt2).
The following example converts the timestamps into RFC3339 timestamps (the timestamp format used by internet standards):
$dt1->set_time_zone("UTC");
say "dt1: " . $dt1->strftime("%Y-%m-%dT%H:%M:%SZ");
$dt2->set_time_zone("UTC");
say "dt2: " . $dt2->strftime("%Y-%m-%dT%H:%M:%SZ");

Convert string "20-May-07" to date and manipulate

In my perl program, I am calculating warranty start date from warranty end date The difference is 3 years. Following is the chunk of code that I have written so far:
use Time::ParseDate;
use DateTime;
use POSIX;
use DateTime::Duration;
my $warranty_expiration_string = "20-May-07";
my $epoch = parsedate($warranty_expiration_string);
my $warranty_expiration = strftime "%D", localtime($epoch); # returns: 05/20/07
How do I deduct 3 years from $warranty_expiration to get $warranty_start date?
I tried,
$warranty_start->subtract(DateTime::Duration->new('years' => 3));
..but it did not work.
I don't understand all the different date/time modules being mixed. You only need some of them, not all of them. If you want to do date math using DateTime anyway, you want something like this:
use DateTime;
use DateTime::Format::Strptime;
my $dateparser = DateTime::Format::Strptime->new( pattern => '%d-%b-%y' );
my $warranty_expiration = $dateparser->parse_datetime($warranty_expiration_string);
my $warranty_start = $warranty_expiration->clone->subtract( years => 3);
Most of the DateTime::Format::* modules are meant to be used with DateTime and I prefer to use those if I can.
You may also want to read more about the ongoing DateTime project and the list of recommended modules at:
http://datetime.perl.org
$warranty_expiration isn't a DateTime, it's a string. You want to do something like:
my $warranty_expiration = DateTime->from_epoch(
epoch => $epoch,
time_zone => 'local',
);
my $warranty_start = $warranty_expiration->clone->subtract(years => 3);
and then you can use $warranty_expiration->strftime("%D") and $warranty_start->strftime("%D") as formatted strings. Also, if you use one of the DateTime::Format modules instead of Time::ParseDate, you will get back a DateTime directly from the parser instead of having to use from_epoch.
No need to instantiate DateTime::Duration for this as the calculation methods expect those parameters directly:
use DateTime;
my $dt = DateTime->now->subtract(years => 3);
print "$dt\n";
Result:
2009-08-30T14:36:27

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;