Convert serial value to date in perl - perl

I am using Spreadsheet::XLSX to convert XLSX into CSV on Linux.
Custom date fields are being converted to numbers. I know that XLSX stores custom dates as serial values. I need to find the way to convert those values into dates/times.
Example:
CSV: 40829
XLSX: 10/13/2011 0:00
So I am trying to figure out how to convert 40829 to 10/13/2011 0:00
I did some research and I was not able to find any (Perl) solution.
I can provide the code if needed.
Please advise.
Thank you,
-Andrey

Excel stores dates and times as a number representing the number of days since 1900-Jan-0, plus a fractional portion of a 24 hour day: ddddd.tttttt.
You could write a function to do the calculations yourself or you could look at some of the modules already posted on cpan for doing this, DateTime::Format::Excel should do what you need and DateTimeX::Format::Excel looks like it would work too.

As per the previous post, this seems like it's the number of days since Jan 1st 1900. By making the 40828'th of January, 1900 (accounting for the off-by-one of being the 1st of January, not the 0th), we get:
use POSIX 'mktime'
my $epoch = mktime 0,0,0, 40829-1,0,0;
print scalar localtime($epoch);
Gives
Thu Oct 13 00:00:00 2011

Or you can use your own functions to convert the dates in EXCEL datevalue and back
sub date2excelvalue {
my($day1, $month, $year, $hour, $min, $sec) = #_;
my #cumul_d_in_m = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365);
my $doy = $cumul_d_in_m[$month - 1] + $day1;
#
full years + your day
for my $y(1900..$year) {
if ($y == $year) {
if ($month <= 2) {
#
dont add manually extra date
if inJanuary or February
last;
}
if ((($y % 4 == 0) && ($y % 100 != 0)) || ($y % 400 == 0) || ($y == 1900)) {
$doy++;#
leap year
}
} else {#
full years
$doy += 365;
if ((($y % 4 == 0) && ($y % 100 != 0)) || ($y % 400 == 0) || ($y == 1900)) {
$doy++;#
leap year
}
}
}#
end
for y# calculate second parts as a fraction of 86400 seconds
my $excel_decimaltimepart = 0;
my $total_seconds_from_time = ($hour * 60 * 60 + $min * 60 + $sec);
if ($total_seconds_from_time == 86400) {
$doy++;#
just add a day
} else {#
add decimal in excel
$excel_decimaltimepart = $total_seconds_from_time / (86400);
$excel_decimaltimepart = ~s / 0\. //;
}
return "$doy\.$excel_decimaltimepart";
}
sub excelvalue2date {
my($excelvalueintegerpart, $excelvaluedecimalpart) = #_;
my #cumul_d_in_m = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365);
my #cumul_d_in_m_leap = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366);
my #cumul_d_in_m_selected;
my($day1, $month, $year, $hour, $min, $sec);
$day1 = 0;#
all days all years
my $days_in_year;
my $acumdays_per_month;
my $daysinmonth;
my $day;
#
full years + your day
for my $y(1900. .3000) {
my $leap_year = 0;#
leap year
my $leap_year_mask = 0;#
leap year
if ((($y % 4 == 0) && ($y % 100 != 0)) || ($y % 400 == 0) || ($y == 1900)) {
$leap_year = 1;#
leap year
#cumul_d_in_m_selected = #cumul_d_in_m_leap;
} else {
$leap_year = 0;#
leap year
#cumul_d_in_m_selected = #cumul_d_in_m;
}
if (($day1 + (365 + $leap_year)) > $excelvalueintegerpart) {
#
found this year $y
$year = $y;
print "year $y\n";
$days_in_year = $excelvalueintegerpart - $day1;
$acumdays_per_month = 0;
print "excelvalueintegerpart $excelvalueintegerpart\n";
print "day1 $day1\n";
print "daysinyear $days_in_year\n";
for my $i(0..$# cumul_d_in_m) {
if ($i == $# cumul_d_in_m) {
$month = $i + 1;#
month 12 December
$day = $days_in_year - $cumul_d_in_m_selected[$i];
last;
} else {
if (($days_in_year > ($cumul_d_in_m_selected[$i])) && ($days_in_year <= ($cumul_d_in_m_selected[$i + 1]))) {
$month = $i + 1;
$day = $days_in_year - $cumul_d_in_m_selected[$i];
last;
}
}
}#
end
for $i months
# end year
last;
} else {#
full years
$day1 += (365 + $leap_year);
}
}#
end
for years interger part comparator
my $total_seconds_inaday;
$total_seconds_inaday = "0\.$excelvaluedecimalpart" * 86400;
$sec = $total_seconds_inaday;
$hour = int($sec / (60 * 60));
$sec -= $hour * (60 * 60);
$min = int($sec / 60);
$sec -= $min * (60);
$sec = int($sec);
return ($day, $month, $year, $hour, $min, $sec);
}
my $excelvariable = date2excelvalue(1, 3, 2018, 14, 14, 30);
print "Excel variable: $excelvariable\n";
my($integerpart, $decimalwithoutzero) = ($1, $2) if ($excelvariable = ~m / (\d + )\.(\d + ) / );
my($day1, $month, $year, $hour, $min, $sec) = excelvalue2date($integerpart, $decimalwithoutzero);
print "Excel Date from value: $day1, $month, $year, $hour, $min, $sec\n";
Enjoy it!

Related

Full manual conversion of Epoch time to Date and Time

I wrote this Perl script to convert date and time from files into epoch time.
The original date format is dd-mm-yyyy hh:MM:ss
The reason I do this is because I have to write these values to a database and only integer values are accepted by the columns.
use strict;
use warnings;
use Time::Local;
$datestring = '07-06-2019 21:13:00';
my ($dd, $mm, $yyyy, $hh, $min, $sec) = split /\W+/, $datestring;
my $epoch = timelocal($sec,$min,$hh,$dd,$mm-1,$yyyy-1900);
print("$epoch\n");
Which translates the date 07-06-2019 21:13:00 to 1559934780
The issue:
The values are now presented in a front-end which is unreadable by the user, the front-end does not have scripting utilities and I can only use numerous different calculation formulas which I can use to make it readable.
Is there a completely manual method, by simply using a calculator/calculation to translate epoch time back into user readable date and time?
The core Time::Piece can convert in both directions simply for your local time zone.
use strict;
use warnings;
use Time::Piece;
my $datestring = '07-06-2019 21:13:00';
my $time = localtime->strptime($datestring, '%d-%m-%Y %H:%M:%S');
my $epoch = $time->epoch;
...
my $time = localtime($epoch);
my $datestring = $time->strftime('%d-%m-%Y %H:%M:%S');
See any standard strftime or strptime man page for the format specifiers that are usually accepted - unfortunately the ones Time::Piece accepts are not documented.
Without access to Time::Piece, which is core since Perl 5.10, you can use the built in localtime function, it's just a little more complicated (like timelocal).
use strict;
use warnings;
my $epoch = 1559934780;
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $epoch;
my $date_string = sprintf '%02d-%02d-%04d %02d:%02d:%02d',
$mday, $mon+1, $year+1900, $hour, $min, $sec;
In very early versions of DateTime.pm you will find a sub _rd2greg that I wrote as jd2greg for Rich Bowen's earlier and long defunct Project Reefknot. That takes a number of days since the beginning of the year 1 and produces a year, month, and day. You can take an epoch time divided by 86400 (seconds in a day) floored and add 719163 (the days up to the year 1970) to pass to it.
Here jd2greg and the corresponding greg2jd:
=head2 jd2greg
($year, $month, $day) = jd2greg( $jd );
Convert number of days on or after Jan 1, 1 CE (Gregorian) to
gregorian year,month,day.
=cut
sub jd2greg {
use integer;
my $d = shift;
my $yadj = 0;
my ( $c, $y, $m );
# add 306 days to make relative to Mar 1, 0; also adjust $d to be within
# a range (1..2**28-1) where our calculations will work with 32bit ints
if ( $d > 2**28 - 307 ) {
# avoid overflow if $d close to maxint
$yadj = ( $d - 146097 + 306 ) / 146097 + 1;
$d -= $yadj * 146097 - 306;
} elsif ( ( $d += 306 ) <= 0 )
{
$yadj =
-( -$d / 146097 + 1 ); # avoid ambiguity in C division of negatives
$d -= $yadj * 146097;
}
$c =
( $d * 4 - 1 ) / 146097; # calc # of centuries $d is after 29 Feb of yr 0
$d -= $c * 146097 / 4; # (4 centuries = 146097 days)
$y = ( $d * 4 - 1 ) / 1461; # calc number of years into the century,
$d -= $y * 1461 / 4; # again March-based (4 yrs =~ 146[01] days)
$m =
( $d * 12 + 1093 ) / 367; # get the month (3..14 represent March through
$d -= ( $m * 367 - 1094 ) / 12; # February of following year)
$y += $c * 100 + $yadj * 400; # get the real year, which is off by
++$y, $m -= 12 if $m > 12; # one if month is January or February
return ( $y, $m, $d );
}
=head2 greg2jd
$jd = greg2jd( $year, $month, $day );
Convert gregorian year,month,day to days on or after Jan 1, 1 CE
(Gregorian). Normalization is performed (e.g. month of 28 means
April two years after given year) for month < 1 or > 12 or day < 1
or > last day of month.
=cut
sub greg2jd {
use integer;
my ( $y, $m, $d ) = #_;
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;
}
# make year positive (oh, for a use integer 'sane_div'!)
if ( $y < 0 ) {
$d -= 146097 * ( $adj = ( 399 - $y ) / 400 );
$y += 400 * $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;
}

Perl: write custom date format to excel spreadsheet

I would like to know how to update cell with custom date format using perl module Spreadsheet::ParseExcel. I am able to get wrong formatted and unformatted value to variable.
For example:
A1 is 11-9-15 23:00 (unformatted 42317.9583333333)
Needed format is 09.11.2015 23:00 (dd.mm.yyyy hh:mm)
$worksheet->AddCell(0, 0, 42317.9583333333, 'dd.mm.yyyy hh:mm');
I need something like command above but it doesn’t work. Cell is 42317.9583333333 not 09.11.2015 23:00.
# INPUT "2015-10-13 23:47" (dd.mm.yyyy hh:mm) -> OUTPUT "42290,9909722222" (excel epoch 1900)
sub dateStringToExcel {
my $date_excel;
my ($date_string) = #_;
if ( $date_string =~ /\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|30|31) ([012345][0-9])\:([012345][0-9])/ ) {
print "stringToexcelDate - OK\n";
my ($year, $month, $day, $hour, $min) = ($date_string =~ /(\d+)-(\d+)-(\d+) (\d+):(\d+)/);
$date_excel = LocaltimeExcel( 0, $min, $hour, $day, $month-1, $year-1900 );
} else {
print "stringToexcelDate - FAILED\n";
$date_excel = "Parsing date string ($date_string) failed!";
};
return $date_excel;
};
I did not know about the LocaltimeExcel! function so i did both conversion functions using plain perl.
sub date2excelvalue {
my($day1, $month, $year, $hour, $min, $sec) = #_;
my #cumul_d_in_m = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365);
my $doy = $cumul_d_in_m[$month - 1] + $day1;
#
full years + your day
for my $y(1900..$year) {
if ($y == $year) {
if ($month <= 2) {
#
dont add manually extra date
if inJanuary or February
last;
}
if ((($y % 4 == 0) && ($y % 100 != 0)) || ($y % 400 == 0) || ($y == 1900)) {
$doy++;#
leap year
}
} else {#
full years
$doy += 365;
if ((($y % 4 == 0) && ($y % 100 != 0)) || ($y % 400 == 0) || ($y == 1900)) {
$doy++;#
leap year
}
}
}#
end
for y# calculate second parts as a fraction of 86400 seconds
my $excel_decimaltimepart = 0;
my $total_seconds_from_time = ($hour * 60 * 60 + $min * 60 + $sec);
if ($total_seconds_from_time == 86400) {
$doy++;#
just add a day
} else {#
add decimal in excel
$excel_decimaltimepart = $total_seconds_from_time / (86400);
$excel_decimaltimepart = ~s / 0\. //;
}
return "$doy\.$excel_decimaltimepart";
}
sub excelvalue2date {
my($excelvalueintegerpart, $excelvaluedecimalpart) = #_;
my #cumul_d_in_m = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365);
my #cumul_d_in_m_leap = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366);
my #cumul_d_in_m_selected;
my($day1, $month, $year, $hour, $min, $sec);
$day1 = 0;#
all days all years
my $days_in_year;
my $acumdays_per_month;
my $daysinmonth;
my $day;
#
full years + your day
for my $y(1900. .3000) {
my $leap_year = 0;#
leap year
my $leap_year_mask = 0;#
leap year
if ((($y % 4 == 0) && ($y % 100 != 0)) || ($y % 400 == 0) || ($y == 1900)) {
$leap_year = 1;#
leap year
#cumul_d_in_m_selected = #cumul_d_in_m_leap;
} else {
$leap_year = 0;#
leap year
#cumul_d_in_m_selected = #cumul_d_in_m;
}
if (($day1 + (365 + $leap_year)) > $excelvalueintegerpart) {
#
found this year $y
$year = $y;
print "year $y\n";
$days_in_year = $excelvalueintegerpart - $day1;
$acumdays_per_month = 0;
print "excelvalueintegerpart $excelvalueintegerpart\n";
print "day1 $day1\n";
print "daysinyear $days_in_year\n";
for my $i(0..$# cumul_d_in_m) {
if ($i == $# cumul_d_in_m) {
$month = $i + 1;#
month 12 December
$day = $days_in_year - $cumul_d_in_m_selected[$i];
last;
} else {
if (($days_in_year > ($cumul_d_in_m_selected[$i])) && ($days_in_year <= ($cumul_d_in_m_selected[$i + 1]))) {
$month = $i + 1;
$day = $days_in_year - $cumul_d_in_m_selected[$i];
last;
}
}
}#
end
for $i months
# end year
last;
} else {#
full years
$day1 += (365 + $leap_year);
}
}#
end
for years interger part comparator
my $total_seconds_inaday;
$total_seconds_inaday = "0\.$excelvaluedecimalpart" * 86400;
$sec = $total_seconds_inaday;
$hour = int($sec / (60 * 60));
$sec -= $hour * (60 * 60);
$min = int($sec / 60);
$sec -= $min * (60);
$sec = int($sec);
return ($day, $month, $year, $hour, $min, $sec);
}
my $excelvariable = date2excelvalue(1, 3, 2018, 14, 14, 30);
print "Excel variable: $excelvariable\n";
my($integerpart, $decimalwithoutzero) = ($1, $2) if ($excelvariable = ~m / (\d + )\.(\d + ) / );
my($day1, $month, $year, $hour, $min, $sec) = excelvalue2date($integerpart, $decimalwithoutzero);
print "Excel Date from value: $day1, $month, $year, $hour, $min, $sec\n";
Enjoy it!

Perl : Getting last Friday of the month

I am trying to get the last Friday of the month. I found out the awesome awk script that could to do this job. I try to port it perl but facing some issues. Any insight would be a great help. I can't use any perl modules apart from the inbuilt one, thats why I have to go through building this stuff.
Thanks for your help.
AWK script :
BEGIN {
split("31,28,31,30,31,30,31,31,30,31,30,31",daynum_array,",") # days per month in non leap year
year = ARGV[1]
if (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)) {
daynum_array[2] = 29
}
y = year - 1
k = 44 + y + int(y/4) + int(6*(y/100)) + int(y/400)
for (m=1; m<=12; m++) {
k += daynum_array[m]
d = daynum_array[m] - (k%7)
printf("%04d-%02d-%02d\n",year,m,d)
}
exit(0)
}
My Perl script :
my #time = localtime;
my ($month, $year) = #time[4, 5];
$year += 1900;
#months = qw( 31 28 31 30 31 30 31 31 30 31 30 31 );
$months[1] = check_leap_year($year) ? 29 : 28;
$y = $year - 1;
$k = 44 + $y + int($y / 4) + int(6 * ($y / 100)) + int($y / 400);
$k += $months[$month];
$d = $months[$month] - ($k % 7);
$month += 1;
printf "%04d-%02d-%02d\n", $year, $month, $d;
sub check_leap_year {
my $year = shift;
return 0 if $year % 4;
return 1 if $year % 100;
return 0 if $year % 400;
return 1;
}
There's a few ways to do this. Using Time::Piece isn't the simplest, it isn't designed for date math, but you don't have to install additional software.
use v5.10;
use strict;
use warnings;
use Time::Piece;
sub get_last_dow_in_month {
my($year, $month, $dow) = #_;
# Get a Time::Piece object at the last day of the month.
my $first_of_the_month = Time::Piece->strptime("$year $month", "%Y %m");
my $last_day = $first_of_the_month->month_last_day;
my $last_of_the_month = Time::Piece->strptime("$year $month $last_day", "%Y %m %d");
# Figure out how many days you need to go back.
my $days_offset = -(($last_of_the_month->day_of_week + (7 - $dow)) % 7);
return $last_of_the_month->mday + $days_offset;
}
say get_last_dow_in_month(2014, 3, 5);
If you need to do more date processing, DateTime is the most comprehensive.
Modules are made to be used. Calc last friday of month on PerlMonks contains some examples.
E.g.
$ perl -MDate::Manip -E 'say UnixDate(ParseDate("last Friday in March 2015"),"Last Friday of the month is %B %E, %Y.")
Last Friday of the month is March 27th, 2015.
Rather than working around the technical limitation, you need to work around the social limitation that is hampering the technical side of your job.
If you are constrained by using core modules only, this is the way to compute it:
use strict;
use warnings;
use Time::Local qw[];
# Computes the last day in the given month which occurs on the given
# day of the week. Returns the day of the month [22, 31].
# 1 <= $month <= 12
# 0 <= $dow <= 6 (0=Sunday)
sub last_dow_in_month {
my ($year, $month, $dow) = #_;
$year += int($month / 12);
$month %= 12;
my $time = Time::Local::timegm(0, 0, 0, 1, $month, $year) - 86400;
my ($mday, $wday) = (gmtime($time))[3,6];
return $mday - ($wday - $dow) % 7;
}
my $year = 2015;
foreach my $month (1..12) {
printf "%.4d-%.2d-%.2d\n",
$year, $month, last_dow_in_month($year, $month, 5);
}
Output:
2015-01-30
2015-02-27
2015-03-27
2015-04-24
2015-05-29
2015-06-26
2015-07-31
2015-08-28
2015-09-25
2015-10-30
2015-11-27
2015-12-25
Using DateTime the code becomes more readable:
use DateTime;
# Computes the last day in the given month which occurs on the given
# day of the week. Returns the day of the month [22, 31].
# 1 <= $month <= 12
# 1 <= $dow <= 7 (1=Monday)
sub last_dow_in_month {
my ($year, $month, $dow) = #_;
my $dt = DateTime->last_day_of_month(year => $year, month => $month);
$dt->subtract(days => ($dt->day_of_week - $dow) % 7);
return $dt->day_of_month;
}
If performance is of essence, Time::Moment can be used to compute it:
use Time::Moment qw[];
# Computes the last day in the given month which occurs on the given
# day of the week. Returns the day of the month [22, 31].
# 1 <= $month <= 12
# 1 <= $dow <= 7 (1=Monday)
sub last_dow_in_month {
my ($year, $month, $dow) = #_;
my $tm = Time::Moment->new(year => $year, month => $month)
->plus_months(1)
->minus_days(1);
return $tm->minus_days(($tm->day_of_week - $dow) % 7)
->day_of_month;
}
A correct implementation of your intended algorithm:
# Computes the last friday in the given month. Returns the day of the
# month [22, 31].
# 1 <= $month <= 12
sub last_friday_in_month {
my ($year, $month) = #_;
my $days = (
[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365],
[0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366],
)[($year % 4) == 0 && ($year % 100 != 0 || $year % 400 == 0)];
my $y = $year - 1;
my $k = 44 + $y + int($y/4) + int(6 * ($y/100)) + int($y/400);
$k += $days->[$month];
return $days->[$month] - $days->[$month - 1] - ($k % 7);
}
This is just for variety. As #Schwern notes, "cal is a clever hack, but it's a crutch that lets the OP avoid learning to use a good date library. Calendaring is hard, use a library."
Assuming you have cal on your system
#!/usr/bin/env perl
use strict;
use warnings;
local $ENV{LC_ALL} = 'C';
my #cal = `cal`;
my $last_friday;
for (my $i = $#cal; $i >= 0; $i -= 1) {
my #dates = split ' ', $cal[$i];
next unless #dates > 5;
$last_friday = $dates[5];
last;
}
print "$last_friday\n";
Or, more succinctly, but somewhat less efficiently:
#!/usr/bin/env perl
use strict;
use warnings;
local $ENV{LC_ALL} = 'C';
my ($last_friday) = grep defined, map +(split)[5], reverse `cal`;
print "$last_friday\n";
Or, even,
#!/usr/bin/env perl
use strict;
use warnings;
use List::Util qw( first );
local $ENV{LC_ALL} = 'C';
my $last_friday = first { defined } map +(split)[5], reverse `cal`;
print "$last_friday\n";

Subtract months from given date

I have a date in the future, from which I have to subtract 3 months at a time until the current date is reached.
The reached date after subtracting the months must be closest to the current date, but has to be in the future.
The day of the month is always the 23rd
i.e.:
future date = 2015/01/23
current date = 2014/06/05
result = 2014/07/23
I'm running Solaris, so don't have access to GNU date.
I tried to do this in Perl, but unfortunately I can only use the Time::Local module:
#!/bin/ksh
m_date="2019/05/23"
m_year=$(echo $m_date|cut -d/ -f1)
m_month=$(echo $m_date|cut -d/ -f2)
m_day=$(echo $m_date|cut -d/ -f3)
export m_year m_month m_day
perl -MTime::Local -le '
$time = timelocal(localtime);
$i = 3;
while (timelocal(0, 0, 0, $ENV{'m_day'}, $ENV{'m_month'} - $i, $ENV{'m_year'}) > $time) {
print scalar(localtime(timelocal(0, 0, 0, $ENV{'m_day'}, $ENV{'m_month'} - $i, $ENV{'m_year'})));
$i += 3;
}
'
This only works for months within one year. Is there any other way I can do this?
It is simple enough to just split the date strings and do the arithmetic on the fields.
use strict;
use warnings;
use 5.010;
my $future = '2015/01/23';
my $current = do {
my #current = localtime;
$current[3] += 1;
$current[5] += 1900;
sprintf '%04d/%02d/%02d', #current[5,4,3];
};
my $result;
for (my $test = $future; $test gt $current; ) {
$result = $test;
my #test = split /\//, $test;
if (($test[1] -= 3) < 1) {
--$test[0];
$test[1] += 12;
}
$test = sprintf '%04d/%02d/%02d', #test;
}
say $result;
output
2014/07/23
Alternatively you could just do the division to calculate how many whole quarters to subtract, like this
use strict;
use warnings;
use 5.010;
my $future = '2015/01/23';
my #current = (localtime)[5,4,3];
$current[1] += 1;
$current[0] += 1900;
my #future = split /\//, $future;
my $months = ($future[0] - $current[0]) * 12 + $future[1] - $current[1];
$months -= 1 if $current[2] >= 23;
my #result = #current;
$result[2] = 23;
$result[1] += $months % 3;
$result[0] += int(($result[1] - 1) / 12);
$result[1] = ($result[1] - 1) % 12 + 1;
my $result = sprintf '%04d/%02d/%02d', #result;
say $result;
The output is identical to that of the previous code
This is your script changed so it should work across multiple years,
perl -MTime::Local -le'
sub nextm {
$ENV{m_year}--, $ENV{m_month} +=12 if ($ENV{m_month} -= 3) <1;
timelocal(0, 0, 0, $ENV{m_day}, $ENV{m_month}, $ENV{m_year});
}
my $time = timelocal(localtime);
while ((my $c=nextm()) > $time) {
print scalar localtime($c);
}
'
Try something like:
#!/usr/bin/perl -w
# just convert the real date that you have to epoch
my $torig = 1558569600;
my $tnow = time;
# 3 months in seconds to use the epoch everywhere
my $estep = 3 * 30 * 24 * 3600;
while(($torig - $estep) > $tnow){
$torig -= $estep;
}
print $torig,"\n";
print scalar localtime($torig),"\n";
The only problem here is that a months is an approximation, if you need the very same day but minus 3 months, you could use DateCalc
I ended up scripting it all in KSH instead of perl, thanks to Borodin's logic.
#!/bin/ksh
set -A c_date $(date '+%Y %m %d')
IFS=/ d="2019/05/23"
set -A m_date $d
[[ ${c_date[2]} -gt ${m_date[2]} ]] && ((c_date[1]+=1))
c_date[2]=${m_date[2]}
c_date[1]=$(( (((${m_date[0]} - ${c_date[0]}) * 12) + (${m_date[1]} - ${c_date[1]})) % 3 + ${c_date[1]} ))
if [[ ${c_date[1]} -gt 12 ]] ; then
((c_date[0]+=1))
((c_date[1]-=12))
fi
echo ${c_date[#]}

Need to convert data as string into an Excel readable format

I have a file with data that includes date strings in this format:
June 11, 2012 3:47:56 PM GMT-07:00
I'm already using a Perl script to manipulate some other data elements from that file then outputting it as a csv for Excel. Rather than fooling about with functions and formulas in Excel to try to translate the date-as-string into something Excel can read, I figure I should be able to do it easier/quicker in Perl.
The purpose / desired end result is a time & date stamp that I can do simple math on in Excel (i.e, get age difference between entries by mathing the time stamps).
To that end, I want to end up with my datestamps looking like this:
6/11/2012 3:47:56 PM
Really I just need to convert the date, the time is perfect, and remove the GMT differential garbage.
I've seen code snippets and references to modules that seem to convert the other way ... i.e., from "6/24/12" to "June 24, 2012", but that's going the wrong direction for me.
I looked up module time::piece in cpan, but don't really understand it. I'm working in a Cygwin exec, so not a real unix system, and don't have much in the way of man pages or perldocs.
You can use module Date::Parse and POSIX functions strftime. There are plenty modules in CPAN, which can parse dates.
Formatting dates with strftime really cool article
use strict;
use Date::Parse;
use POSIX qw/strftime/;
my $time = str2time( 'June 11, 2012 3:47:56 PM GMT-07:00' );
my $date = strftime "%m/%d/%Y %H:%M:%S %p", localtime($time);
print $date;
Good luck!
If you original string is: $timestring, then this should work (not tested):
my %months = (January => '1', February =>'2', March => '3', ...);
...
etc.
$timestring =~ s<^(w+)\x20(\d{1,2}),\x20(\d{4})(\x20\(?:\d{1,2}\:){2}\d{1,2}\x20PM).*$><"$months{$1}/$2/$3$4">eeg
use DateTime;
use DateTime::Format::Strptime;
# YOU MUST ADAPT THIS PATTERN
my $pat = "%b %d, %Y"; #incomplete
my $d = DateTime::Format::Strptime->new( pattern => $pat, on_error => 'croak' );
my $dt = $d->parse_datetime($strdate);
say $dt->mdy("/") . " " . $dt->hms(":") . " " . $dt->am_or_pm;
But note that Excel prefers Datetime types in ISO Format:
say $dt->ymd('-') . 'T' . $dt->hms(':');
For finetuning the pattern $pat, read the module documentation for DateTime::Format::Strptime.
It works best for parsing input data that is very uniform. Encounter the slightest deviation and the module won't parse it (you can work around this, though)
Many people have their favorite time parsing techniques. I like Time::Piece because it comes with Perl (at least any version over 5.10):
my $time_string = "June 11, 2012 3:47:56 PM GMT-7:00";
my $time_string =~ / GMT.*$//; # That "GMT-7:00" messes things up!
say $time_string # June 11, 2012 3:47:56 PM GMT
my $time = Time::Piece->strptime(
$time_string, "%B %d, %Y %l:%M:%S %p" );
say $time->strftime("%D %l:%M:%S %p");
The $foo->bar is object oriented coding style which is becoming the way of the future in Perl, so you better get use to it.
It's not really all that complex. Basically, you create an container that holds all your data. Sort of the way a hash can hold various pieces of information.
When I do this:
my $time = Time::Piece->strptime( "$time_string", "$time_format" );
I'm creating a Time::Piece object (nee container) called $time which stores the time.
When you say $time->Weekday, you are actually executing a subroutine called Weekday that takes your $time container, extracts the information from it, parses the time, and return the weekday.
The strptime (STRing Parse TIME) constructor (which is the function that creates your container) is taking your time string (the first argument), and the format it is in (the second argument) and creating that $time object. The various %M and %d specify particular time fields. These can be found in the strptime manpage.
You can play around with these formats with the Unix date command:
$ date "+%m/%d/%y"
08/23/13
$ date "+%m/%d/%Y"
08/23/2013
$ date "%Y-%m-%d"
2013-08-23
That might make you feel more comfortable with them.
The strftime (STRing Format TIME) method (nee subroutine) is the opposite of strptime. This takes the time (which is in $time and returns the time in the format you specified.
If you have problems installing new extra packages you can do that using plain per. Once you have the excel date serial value e.g 61,59340278 you can use Excel to format the appearance of that number in a custom date.
For example:
61,59340278 is displayed in Excel using custom date format
CUSTOM Cell format: TT.MM.JJJJ hh:mm:ss
as> 01.03.1900 14:14:30
Below the functions
sub date2excelvalue {
my($day1, $month, $year, $hour, $min, $sec) = #_;
my #cumul_d_in_m = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365);
my $doy = $cumul_d_in_m[$month - 1] + $day1;
#
full years + your day
for my $y(1900..$year) {
if ($y == $year) {
if ($month <= 2) {
#
dont add manually extra date
if inJanuary or February
last;
}
if ((($y % 4 == 0) && ($y % 100 != 0)) || ($y % 400 == 0) || ($y == 1900)) {
$doy++;#
leap year
}
} else {#
full years
$doy += 365;
if ((($y % 4 == 0) && ($y % 100 != 0)) || ($y % 400 == 0) || ($y == 1900)) {
$doy++;#
leap year
}
}
}#
end
for y# calculate second parts as a fraction of 86400 seconds
my $excel_decimaltimepart = 0;
my $total_seconds_from_time = ($hour * 60 * 60 + $min * 60 + $sec);
if ($total_seconds_from_time == 86400) {
$doy++;#
just add a day
} else {#
add decimal in excel
$excel_decimaltimepart = $total_seconds_from_time / (86400);
$excel_decimaltimepart = ~s / 0\. //;
}
return "$doy\.$excel_decimaltimepart";
}
sub excelvalue2date {
my($excelvalueintegerpart, $excelvaluedecimalpart) = #_;
my #cumul_d_in_m = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365);
my #cumul_d_in_m_leap = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366);
my #cumul_d_in_m_selected;
my($day1, $month, $year, $hour, $min, $sec);
$day1 = 0;#
all days all years
my $days_in_year;
my $acumdays_per_month;
my $daysinmonth;
my $day;
#
full years + your day
for my $y(1900. .3000) {
my $leap_year = 0;#
leap year
my $leap_year_mask = 0;#
leap year
if ((($y % 4 == 0) && ($y % 100 != 0)) || ($y % 400 == 0) || ($y == 1900)) {
$leap_year = 1;#
leap year
#cumul_d_in_m_selected = #cumul_d_in_m_leap;
} else {
$leap_year = 0;#
leap year
#cumul_d_in_m_selected = #cumul_d_in_m;
}
if (($day1 + (365 + $leap_year)) > $excelvalueintegerpart) {
#
found this year $y
$year = $y;
print "year $y\n";
$days_in_year = $excelvalueintegerpart - $day1;
$acumdays_per_month = 0;
print "excelvalueintegerpart $excelvalueintegerpart\n";
print "day1 $day1\n";
print "daysinyear $days_in_year\n";
for my $i(0..$# cumul_d_in_m) {
if ($i == $# cumul_d_in_m) {
$month = $i + 1;#
month 12 December
$day = $days_in_year - $cumul_d_in_m_selected[$i];
last;
} else {
if (($days_in_year > ($cumul_d_in_m_selected[$i])) && ($days_in_year <= ($cumul_d_in_m_selected[$i + 1]))) {
$month = $i + 1;
$day = $days_in_year - $cumul_d_in_m_selected[$i];
last;
}
}
}#
end
for $i months
# end year
last;
} else {#
full years
$day1 += (365 + $leap_year);
}
}#
end
for years interger part comparator
my $total_seconds_inaday;
$total_seconds_inaday = "0\.$excelvaluedecimalpart" * 86400;
$sec = $total_seconds_inaday;
$hour = int($sec / (60 * 60));
$sec -= $hour * (60 * 60);
$min = int($sec / 60);
$sec -= $min * (60);
$sec = int($sec);
return ($day, $month, $year, $hour, $min, $sec);
}
my $excelvariable = date2excelvalue(1, 3, 2018, 14, 14, 30);
print "Excel variable: $excelvariable\n";
my($integerpart, $decimalwithoutzero) = ($1, $2) if ($excelvariable = ~m / (\d + )\.(\d + ) / );
my($day1, $month, $year, $hour, $min, $sec) = excelvalue2date($integerpart, $decimalwithoutzero);
print "Excel Date from value: $day1, $month, $year, $hour, $min, $sec\n";
Enjoy it!