Calculate number of days between two dates (without using modules) - perl

I searched around for this a lot, and it appears all solutions are making use of some module or the other.
I have 2 dates in yyyymmdd format. I'd like to know of a simple way to calculate the number of days between these dates, without using any modules.
Example:
Date 1: 20130625
Date 2: 20130705
Number of days = 10
PS: I cannot use any modules due to restrictions on the server I will be running the script on. Perl version - 5.8.4

Expression returns 10.
(
Time::Piece->strptime('20130705', '%Y%m%d')
- Time::Piece->strptime('20130625', '%Y%m%d')
)->days
Time::Piece is part of the core Perl distribution since v5.9.5.

sub yyyymmdd_to_rata_die {
use integer;
my ( $y, $m, $d ) = $_[0] =~ /\A([0-9]{4})([0-9]{2})([0-9]{2})\z/
or return;
my $adj;
# make month in range 3..14 (treat Jan & Feb as months 13..14 of prev year)
if ( $m <= 2 ) {
$y -= ( $adj = ( 14 - $m ) / 12 );
$m += 12 * $adj;
}
elsif ( $m > 14 ) {
$y += ( $adj = ( $m - 3 ) / 12 );
$m -= 12 * $adj;
}
# add: day of month, days of previous 0-11 month period that began w/March,
# days of previous 0-399 year period that began w/March of a 400-multiple
# year), days of any 400-year periods before that, and 306 days to adjust
# from Mar 1, year 0-relative to Jan 1, year 1-relative (whew)
$d += ( $m * 367 - 1094 ) / 12 + $y % 100 * 1461 / 4 + ( $y / 100 * 36524 + $y / 400 ) - 306;
}
print yyyymmdd_to_rata_die(20130705) - yyyymmdd_to_rata_die(20130625);

Its simple:
1) for each date:
1.1) turn years into ydays
1.2) turn months into mdays
1.3) make sum days + mdays + ydays
2) substract that two values

Related

While loop until time is reached

I have an application that loops reading a big text file and runs for at least 48 hours.
I would like to stop its execution from 23.00 to 08.00 (11pm to 8am) and then continue its loop during the day.
I tried the following code, but it doesn't exit from the loop.
Edit after Dave Cross' reply
my $min = (localtime)[1];
my $hour = (localtime)[2];
while ( $hour > 18 and $min > 0 ) {
sleep(1);
$hour = (localtime)[2];
$min = (localtime)[1];
}
This doesn't work as expected. Now it's 18:48 and the loop exits immediately unless I use one hour before:
while ( $hour > 17 and $min > 0 )
You need to check the time more than once :-)
And rather than calling strftime(), why not just use the return value from localtime()?
my $hour = (localtime)[2];
while ($hour >= 23 or $hour < 8) {
sleep(1);
$hour = (localtime)[2];
}
Update: A more efficient approach might be to work out how long to sleep() in order to wake up at 8am. A naive approach would be something like this:
if ((localtime)[2]) >= 23) {
sleep(9 * 60 * 60); # sleep for nine hours
}

How can I get the next weekday in Perl?

I am trying to calculate the next day (e.g. Monday/Tuesday/Wednesday etc) using the variables below.
Example: localtime day is Wednesday and I require Thursday, the offset being +1, day is Thursday and I require Monday, offset being +4
my #days = qw(Sunday Monday Tuesday Wednesday Thursday Friday Saturday);
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime
Is there some sort of algorithm I can work out to do this? I am unable to use DateTime & Time::Piece library in Perl as a restriction.
I was using:
my $daysToAdd = 8 - $wday; # picks up how many days there is until next Monday
For basic date handling in Perl, throw away localtime and use the built in Time::Piece class. It will do basic date math.
use strict;
use warnings;
use v5.10;
use Time::Piece;
use Time::Seconds; # Some useful constants like ONE_DAY
my $now = Time::Piece->new;
say $now;
my $tomorrow = $now + ONE_DAY;
say $tomorrow;
As far as calculating the offset between two week days, map them to numbers and use those numbers. Sunday is 0, Monday is 1, and so on. The algorithm for figuring out how many days you have to advance between two weekdays is...
(7 - $current_dow + $new_dow) % 7
To expand that out: 7 - $current_dow is how many days from the current day to the end of the week. Then you add your desired day onto that. The whole thing is mod 7 (so 8 becomes 1) to handle the case of moving just a few days forward (ie. Wednesday to Friday).
Doing it for Friday to Thursday is...
(7 - 5 + 4) % 7
(2 + 4) % 7
6 % 7
6
Doing it for Friday to Saturday is...
(7 - 5 + 6) % 7
(2 + 6) % 7
8 % 7
1
And here's the code.
my $day = 4; # Thursday
my $days_offset = (7 - $now->day_of_week + $day) % 7;
my $thursday = $now + ($days_offset * ONE_DAY);
say $thursday;

Converting times before the year 999 to epoch

In Perl a function called timelocal exists to convert times to epoch.
example: my $epoch = timelocal($sec, $min, $hour, $mday, $mon, $year)
This function however appears to be inherently flawed when dealing with times very far in the past (before the year 999) - see the section on Year Value Interpretation. To make things worse, the way it processes 2 digit years complicates things even more...
Given a time before the year 999 how can I accurately convert it to its corresponding epoch value?
Given a time before the year 999 how can I accurately convert it to its corresponding epoch value?
You can't with Time::Local. timegm (which is used by timelocal) contains the following:
if ( $year >= 1000 ) {
$year -= 1900;
}
elsif ( $year < 100 and $year >= 0 ) {
$year += ( $year > $Breakpoint ) ? $Century : $NextCentury;
}
If the year is between 0 and 100, it's automatically converted to a year in the current century as described in the documentation; years between 100 and 999 are treated as offsets from 1900. You can't get around that without hacking the source.
If your perl was compiled to use 64-bit integers*, you can use the DateTime module instead:
use strict;
use warnings 'all';
use 5.010;
use DateTime;
my $dt = DateTime->new(
year => 1,
month => 1,
day => 1,
hour => 0,
minute => 0,
second => 0,
time_zone => 'UTC'
);
say $dt->epoch;
Output:
-62135596800
Note that the Gregorian calendar wasn't even adopted until 1582, so DateTime uses what's called the "proleptic Gregorian calendar" by simply extending it backwards from 1582.
* With 32-bit integers, dates too far in the past or future will cause integer overflow. Your perl supports 64-bit integers if use64bitint=define appears in the output to perl -V (with a capital 'V').
Looking at the votes and reviews, the DateTime module would seem to be the authoritative, go-to module for this sort of stuff. Unfortunately its $dt->epoch() documentation comes with these caveats;
Since the epoch does not account for leap seconds, the epoch time for
1972-12-31T23:59:60 (UTC) is exactly the same as that for 1973-01-01T00:00:00.
This module uses Time::Local to calculate the epoch, which may or may not
handle epochs before 1904 or after 2038 (depending on the size of your system's
integers, and whether or not Perl was compiled with 64-bit int support).
It would appear these are the limits you going to have to work within.
Having said that, this comment is probably a sensible warning for users who
Are using a machine with 32-bit ints; or
Have a low error tolerance even for "old" dates
The first is going to be a problem if you have a 32-bit machine. The range (in years) for a signed 32-bit based epoch is around 2^31 / (3600*24*365) or (only) 68 years to/from 1970 (presuming a unix epoch). For a 64 bit int however, it becomes 290,000 years to/from 1970 - which would be ok, I presume. :-)
Only you can say if the second issue is going to be a problem.
Herewith are the results of a back-of-the-envelope examination of the degree of error;
$ perl -MDateTime -E 'say DateTime->new( year => 0 )->epoch / (365.25 * 24 * 3600)'
-1969.96030116359 # Year 0ad is 1969.96 years before 1970
$ perl -MDateTime -E 'say DateTime->new( year => -1000 )->epoch / (365.25*24*3600)'
-2969.93839835729 # year 1000bc is 2969.94 years before 1970
$ perl -MDateTime -E 'say ((DateTime->new( year => -1000 )->epoch - DateTime->new( year => 0 )->epoch ) / (365.25*24*3600))'
-999.978097193703 # 1,000bc has an error of 0.022 years
$ perl -MDateTime -E 'say 1000*365.25 + ((DateTime->new( year => -1000 )->epoch - DateTime->new( year => 0 )->epoch ) / (24 * 3600))'
8 # ... or 8 days
$
NOTE: I don't know how much of this "error" is due to the way I'm examining it - a year is not 365.25 days. In fact, let me correct that - I took a better definition of days in a year from here and we get;
$ perl -MDateTime -E 'say 1000*365.242189 + ((DateTime->new( year => -1000 )->epoch - DateTime->new( year => 0 )->epoch ) / (24 * 3600))'
0.189000000013039
So, an error of something-like 0.2 days when working with dates around 1,000bc.
In short, if you have 64 bit machine, you should be fine.
The Gregorian calendar repeat completely every 146,097 days, which equals 400 years. We can map a year less than 1000 to an equivalent year within the cycle. The following implementation map a year less than 1000 to a year in the third cycle, for example 0001 maps to 1201.
#!/usr/bin/perl
use strict;
use warnings;
use Time::Local qw[timegm timelocal];
use constant CYCLE_YEARS => 400;
use constant CYCLE_DAYS => 146097;
use constant CYCLE_SECONDS => CYCLE_DAYS * 86400;
sub mytimelocal {
my ($sec, $min, $hour, $mday, $month, $year) = #_;
my $adjust = 0;
if ($year < 1000) {
my $cycles = 3 - int($year/CYCLE_YEARS) + ($year < 0);
$year += $cycles * CYCLE_YEARS;
$adjust = $cycles * CYCLE_SECONDS;
}
return timelocal($sec, $min, $hour, $mday, $month, $year) - $adjust;
}
use Test::More tests => CYCLE_DAYS;
use constant STD_OFFSET =>
timelocal(0, 0, 0, 1, 0, 1200) - timegm(0, 0, 0, 1, 0, 1200);
my $seconds = -5 * CYCLE_SECONDS; # -0030-01-01
while ($seconds < -4 * CYCLE_SECONDS) { # 0370-01-01
my #tm = gmtime($seconds);
$tm[5] += 1900;
my $got = mytimelocal(#tm);
my $exp = $seconds + STD_OFFSET;
is($got, $exp, scalar gmtime($seconds));
$seconds += 86400;
}

Why does this perl DateTime math produce unexpected results?

#!/usr/bin/perl
use DateTime;
$a = DateTime->new(year=>1952,month=>10,day=>21);
$b = DateTime->new(year=>2015,month=>10,day=>31);
$dif = $b-$a;
print $dif->years() ." ". $dif->months() ." ". $dif->days();
# result: 63 0 3
Where does it get the 3 days from? My expectation is 63 0 10.
#!/usr/bin/perl
use DateTime;
$a = DateTime->new(year=>1952,month=>11,day=>1);
$b = DateTime->new(year=>2015,month=>10,day=>31);
$dif = $b-$a;
print $dif->years() ." ". $dif->months() ." ". $dif->days();
# result 62 11 2
My expectation for this one is 62 11 31 or so.
I am trying to do some basic date of birth to age math. The month and year seem to work as I expect but the day seems unpredictable. I have read the CPAN documentation but I still do not understand.
$dif->years, $diff->months and in particular $diff->days do not do what you expect. From the DateTime::Duration documentation...
These methods return numbers indicating how many of the given unit the
object represents, after having done a conversion to any larger units. For
example, days are first converted to weeks, and then the remainder is
returned. These numbers are always positive.
Here's what each method returns:
$dur->years() == abs( $dur->in_units('years') )
$dur->months() == abs( ( $dur->in_units( 'months', 'years' ) )[0] )
$dur->weeks() == abs( $dur->in_units( 'weeks' ) )
$dur->days() == abs( ( $dur->in_units( 'days', 'weeks' ) )[0] )
If you find this confusing, so do I.
What you want is in_units.
# 63 0 10
say join " ", $dif->in_units('years', 'months', 'days');

how to compare timestamp in array values iteratively

I have an array
#array = ( 'Apr 11 21:14:25',
'Apr 11 21:10:10',
'Apr 11 21:09:10',
'Apr 11 21:07:10',
);
Here I want to comapare time stamps in a array
Process:
First value in a array should compare with second value to check time difference of 2 minutes or not?
second value in a array should compare with third value and again should check time difference of 2 minutes.
Same process should go on
Any Ideas on How to acheive this in perl?
We don't really just hand out answers without you making an effort first. But it's lunchtime and I wanted a simple programming problem to work on.
#!/usr/bin/perl
use strict;
use warnings;
use 5.010;
use Time::Piece;
my #times = (
'Apr 11 21:14:25',
'Apr 11 21:10:10',
'Apr 11 21:09:10',
'Apr 11 21:07:10',
);
# Need a year for this to make sense. Let's use
# the current one.
my $year = localtime->year;
# Convert the string dates to Time::Piece objects
my #timepieces = map {
Time::Piece->strptime("$year $_", '%Y %b %d %H:%M:%S')
} #times;
for (1 .. $#times) {
say "Comparing $times[$_ - 1] with $times[$_]";
# Use abs() so we don't care which is larger.
my $seconds = abs($timepieces[$_ - 1] - $timepieces[$_]);
if ($seconds == 120) {
say 'Exactly two minutes difference';
} elsif ($seconds > 120) {
say 'More than two minutes difference';
} else {
say 'Less than two minutes difference';
}
}