How to calculate time difference in perl using POSIX module? - perl

I am new to perl scripting. I have a need where in I require to find the difference of two dates in the Days/Hrs/minutes/seconds, provided I just have POSIX module ( I can't use Time::Piece or DateTime or... module)
Like,
$date1 = Tue Nov 30 10:53:38 2021;
$date2 = Fri Dec 10 02:12:25 2021;
$output = $date2 - $date1 :: here $output should be 09 days, 3hrs, 19 mins, 47 secs.
Can you please tell me how do we achieve this: Parsing, calculation?
Your help will be highly appreciated.
Thanks!

Algorithm to a solution of the problem can be achieved with POSIX::mktime function.
Input dates require some 'massaging' to bring provided dates to expected representation by mktime function what is achieved by date2epoch function which returns representation of the date in seconds.
Once dates was converted to seconds it is a matter of trivial computation to obtain time difference represented in days/hours/minutes/seconds.
Note #1: It is assumed that dates obtained in same timezone.
Note #2: OP's date difference computation is incorrect
use strict;
use warnings;
use feature 'say';
use POSIX;
my $date1 = 'Tue Nov 30 10:53:38 2021';
my $date2 = 'Fri Dec 10 02:12:25 2021';
my $diff = date_diff($date1,$date2);
printf "Date difference: %02d days %02d hours %02d min %02d sec\n",
$diff->#{qw/days hours min sec/};
sub date_diff {
my $date1 = shift;
my $date2 = shift;
my $diff;
my $sec_diff = date2epoch($date2) - date2epoch($date1);
$diff->{sec} = $sec_diff % 60;
$diff->{min} = ($sec_diff % 3600 - $diff->{sec} ) / 60;
$diff->{hours} = $sec_diff / 3600 % 24;
$diff->{days} = int( $sec_diff / ( 24 * 3600 ) );
return $diff;
}
sub date2epoch {
my $str_date = shift;
my($months,$date,$epoch);
$months->#{qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/} = (0..11);
$date->#{qw/week_day month month_day time year/} = split(' ', $str_date);
$date->#{qw/hour min sec/} = split(':', $date->{time});
$date->{month} = $months->{ $date->{month} };
$date->{year} -= 1900;
$epoch = POSIX::mktime( $date->#{qw/sec min hour month_day month year/} );
return $epoch;
}
Output
Date difference: 09 days 15 hours 18 min 47 sec

Related

Time interval between two dates with Perl

I'm adding two dates and trying to calculate the time, but I'm getting the following error:
Error parsing time at /usr/local/lib/x86_64-linux-gnu/perl/5.30.0/Time/Piece.pm line 598.
I install Time::Piece with cpan: cpan Time::Piece.
This my code:
our #months = qw( 01 02 03 04 05 06 07 08 09 10 11 12 );
our #days = qw(Domingo Segunda Treça Quarta Quinta Sexta Sabado Domingo);
($sec,$min,$hour,$mday,$mon,$year,$wday,$day,$isdst) = localtime();
our $ano = "2021";
our $day = "$mday";
our $mes = $months[$mon];
our $data = $mes."-".$day."-".$ano;
our $horario = $hour.":".$min.":".$sec;
our $horario2 = $hour.":".$min.":".$sec;
our $data1 = $ano."-".$mes."-".$day;
our $data2 = $day."/".$mes."/".$ano;
our $str1 = 'Execution completed at '.$data2.' '.$horario.' AM';
our #mes = qw( Jan Feb Mar APr May Jun Jul Agu Sep Oct Nov Dec );
our #days = qw(Domingo Segunda Treça Quarta Quinta Sexta Sabado Domingo);
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();
$nomeMes = $mes[$mon];
our #mes = qw( Jan Feb Mar APr May Jun Jul Agu Sep Oct Nov Dec );
our #days = qw(Domingo Segunda Treça Quarta Quinta Sexta Sabado Domingo);
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();
our $data2 = $day."/".$mes."/".$ano;
our $horario = $hour.":".$min.":".$sec;
my $str2 = 'Execution completed at '.$data2.' '.$horario.' AM';
my #times = map Time::Piece->strptime(/(\d.+M)/, '%m/%d/%Y %H:%M:%S %p'), $str1, $str2;
my $delta = $times[1] - $times[0];
$tempo = $delta->pretty;
What am I doing wrong? What can I do to make this function work?
The matched pattern of $str1 is 20/12/2021 13:58:3 AM
Problems:
There's no 20th month
There's no 13 AM
Can give the wrong answer near a switch from Daylight-Saving Time.
Also, there's a couple of problems strptime ignores:
You should be using %I instead of %H for 12-hour time.
There's a lack of leading zeros where they are normally expected (minutes and seconds).
You appear to be asking the following:
Given the year, month, day, hour, minute and second components of a local time, how do I obtain the corresponding epoch time so I can perform a difference?
To achieve this, use Time::Local's timelocal*.
use Time::Local qw( timelocal_posix );
my $time = timelocal_posix( $sec, $min, $hour, $day, $month - 1, $year - 1900 );
You could also use DateTime. This more powerful module can give you differences in amounts other than seconds.
Either way, you will still have problems near a switch from DST. There's simply not enough information to address that. That's the problem with dealing with local times with no offset.
I use the script:
our $str2 = $ano.'/'.$mes.'/'.$day.' '.$hour.':'.$min.':'.$sec.'.267-05:00';
my #times = map Time::Piece->strptime( s/\..*//r, '%Y/%m/%d %H:%M:%S'), $str1, $str2;
our $delta = $times[1] - $times[0];
print $delta->pretty;
It's work.
Thanks very much Ikegami for help.

perl to open file with yesterday's date in localtime()

i need to open files with todays date and yesterdays date i can open todays file ok but am don't know how to open yesterdays, i am using localtime because my perl verson is 5.8.8.
so the other time/date modules are not available
this is what i have so far
#!/usr/local/bin/perl
#months = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
#days = qw(Sun Mon Tue Wed Thu Fri Sat Sun);
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();
$year = $year+1900;
print "c:\\DBR_$year\\$months[$mon]\\Failures_output\\$mday$months[$mon]report.csv"
|| die "can't open output file for reading: $!";
this prints
c:\DBR_2014\May\Failures_output\5Mayreport.csv
now how do i open same files with yesterdays date
A localtime/mktime roundtrip will give you the epoch number for midnight at the start of yesterday (i.e. the time where mday is 1 less than now)
use POSIX 'mktime';
my #now = localtime();
my $yesterday = mktime 0, 0, 0, $now[3]-1, $now[4], $now[5];
You can then put this into strftime to give you the time string; you don't need to have an array of months like that
use POSIX 'strftime';
my $path = strftime("c:\\DBR_%Y\\%b\\Failures_output\\%d%breport.csv", localtime($yesterday));
(You don't have to worry about the case where mday is already 1 because mktime will handle that properly).
localtime can take an epoch seconds argument, so do get yesterday's date, just call
#yesterday = localtime( time - 86400 ); # 86400 = 24 * 60 * 60

Why does my Time::Piece code give strange results?

I am trying to do a basic comparison of two dates in Perl. The Current DateTime and the Past time are correct but the subtraction gives incorrect results. The Difference should be ~24 hours yet it returns ~13 hours. Any idea why and how to fix it? thanks.
use Time::Piece;
my $now = Time::Piece->new;
my $then = Time::Piece->strptime("2014-04-14 16:30:20", "%Y-%m-%d %H:%M:%S");
my $diff = $now - $then;
print "Current time: $now\n";
print "Past time: $then\n";
print "Diff in Seconds:", $diff, "\n";
print "Pretty Diff:", $diff->pretty, "\n";
Results
------
Current time: Tue Apr 15 16:13:39 2014
Past time: Mon Apr 14 16:30:20 2014
Diff in Seconds:49399
Pretty Diff:13 hours, 43 minutes, 19 seconds
The two timepoints are in different timezones. So the difference is in fact correct. You can see that with
print $now->tzoffset, "\n"; # 7200 (I am in UTC +2 hence have 7200s offset)
print $then->tzoffset, "\n"; # 0
So basically $then is a UTC time while $now is in whatever timezone your environment thinks it is in.
To fix that, you need to decide on what timezone you want.
As DeVadder has already stated, it's because Time::Piece defaults to UTC for parsed times.
Assuming you want everything done using your localtime, you can actually encourage the parsed times to inherit their Timezone from local like so:
use Time::Piece;
use strict;
use warnings;
my $now = Time::Piece->new;
my $then = localtime->strptime("2014-04-14 16:30:20", "%Y-%m-%d %H:%M:%S");
my $diff = $now - $then;
print "Current time: $now\n";
print "Past time: $then\n";
print "Diff in Seconds:", $diff, "\n";
print "Pretty Diff:", $diff->pretty, "\n";
Outputs:
Current time: Tue Apr 15 17:12:08 2014
Past time: Mon Apr 14 16:30:20 2014
Diff in Seconds:88908
Pretty Diff:1 days, 0 hours, 41 minutes, 48 seconds

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.

Calculating a delta of years from a date

I am trying to figure out a way to calculate the year of birth for records when given the age to two decimals at a given date - in Perl.
To illustrate this example consider these two records:
date, age at date
25 Nov 2005, 74.23
21 Jan 2007, 75.38
What I want to do is get the year of birth based on those records - it should be, in theory, consistent. The problem is that when I try to derive it by calculating the difference between the year in the date field minus the age, I run into rounding errors making the results look wrong while they are in fact correct.
I have tried using some "clever" combination of int() or sprintf() to round things up but to not avail. I have looked at Date::Calc but cant see something I can use.
p.s. As many dates are pre-1970, I cannot not unfortunately use UNIX epoch for this.
Have you tried DateTime? It'll handle parsing as well as subtraction.
Perl's gmtime and localtime functions have no problem handling negative input and dates before 1970.
use Time::Local;
$time = timegm(0,0,0,25,11-1,2005-1900); # 25 Nov 2005
$birthtime = $time - (365.25 * 86400) * 74.23; # ~74.23 years
print scalar gmtime($birthtime); # ==> Wed Sep 2 11:49:12 1931
The actual birthdate could be different by a few days, since one one-hundredth of a year only gives you a resolution of 3-4 days.
Use DateTime and DateTime::Duration.
When you substract a DateTime::Duration from a DateTime you get an other DateTime.
use strict;
use warnings;
use DateTime::Format::Strptime;
use DateTime::Duration;
my $fmt = DateTime::Format::Strptime->new(
pattern => '%d %b %Y',
locale => 'en_US',
);
my $start = $fmt->parse_datetime($ARGV[0]);
my $age = DateTime::Duration->new(years => $ARGV[1]);
my $birth = $start - $age;
print $fmt->format_datetime($birth), "\n";
Here is an example on how to invoke it:
$ perl birth.pl "25 Nov 2005" 74.23
25 Sep 1931
$ perl birth.pl "21 Jan 2007" 75.38
21 Sep 1931
I'd second Oesor's recommendation (second time today), and reiterate mobrule's reminder that perl handles negative dates. So DateTime is preferable.
But I would like to illustrate that this can be done with POSIX::mktime:
my ( $year1, $mon1, $day1 ) = qw<1944 7 1>;
my ( $year2, $mon2, $day2 ) = qw<2006 5 4>;
my $time1 = POSIX::mktime( (0) x 3, $day1, $mon1 - 1, 72 );
my $time2 = POSIX::mktime( (0) x 3, $day2, $mon2 - 1, 72 );
my $years = $year2 - $year1 - ( $time2 < $time1 ? 1 : 0 );
# 61 years
The caveat is that perl's internal clock handles dates back to December 14th, 1902 (actually 13th, after noon and before 6 PM), before which mktime starts returning undef. So for 99% of the people alive today, this will probably do.
Pointless trivia: scalar localtime( 0x80000000 ) : 'Fri Dec 13 15:45:52 1901' <- that's the cutoff ( 0x80000000 being 2s-complement minimum integer )