Code:
#!/usr/bin/perl
use POSIX;
use strict;
use warnings;
my $cmdstr="%Y-%m-%d %H:%M:%S"; # see strftime
my ($input, $arg, $sign, $ref,$offset,$quiet,$n)=("", undef, 0, time,0,0,0);
# $lt[SEC] is seconds, etc. See perldoc -f localtime
use constant { SEC=>0, MIN=>1, HOUR=>2,DAY=>3, MON=>4, YEAR=>5,WDAY=>6 };
# Titles for localtime() array elements
my @title=("Seconds","Minutes","Hours","Day","Month","Year","Weekday");
# Times stored in the format of 'perldoc -f localtime'.
# In the case of @changed, only elements that were altered in @lt are
# set,other values are undefined.
my (@lt, @changed);
# Lookups for mon/tues/wed jan/feb/mar names into day and month numbers
my (%month, %days);
my %mult=("second" => 1, "minute" => 60, "hour" => 60*60, "day" => 60*60*24,
"week"=>60*60*24*7, "year" => 31536000 );
# Alter a value in @lt and copy that value into the same index of @changed
sub set {
my ($i,$v)=(shift,shift);
$lt[$i]=$changed[$i]=$v;
}
# Takes an hour in AM, returns a number in 24-hour time
sub am { if($_[0] == 12) { return($_[0] - 12); } return($_[0]); }
# Takes an hour in PM, returns an hour in 24-hour time
sub pm { if($_[0] >= 1) { return($_[0] + 12); } return($_[0]); }
# loadhash(\%hash, "A;B", "a;b")
# sets $hash{A}=0, $hash{B}=1, $hash{a}=0, $hash{b}=1
sub loadhash {
my ($h,$n)=(shift,0);
foreach(@_) {
$n=0; foreach(split('[;\n]', lc($_))) { ${$h}{$_}=$n++; }
}
}
# Parse commandline arguments
while(defined($arg=shift)) {
if($arg =~ /^--date=/) { $input=substr($arg, 7); }
elsif($arg =~/^\+/) { $cmdstr=substr($arg,1); }
elsif($arg eq "-q") { $quiet=1; }
elsif($arg eq "-d") { $input=shift; }
elsif(($arg eq "-r") || ($arg =~ /^--reference=(.*)$/))
{
if(defined($1)) { $arg=$1; }
else { $arg=shift; }
my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
$atime,$mtime,$ctime,$blksize,$blocks)
= stat($arg);
defined($dev) || die("No such file $arg");
$ref=$mtime;
}
elsif($arg =~/^-h|--help|--version$/) {
print STDERR <<"EOT";
date.pl v0.0.8, Tyler Montbriand, 2016. Free PERL date calc/converter.
-d "time string" string like "YYYYMMDD", "YYYY/MM/DD",
"HHMMSS", "HH:MM:SS", "\@epoch", "- 3 days",
"Mar 3 2016 1:16:09 AM",
etc. You can string them together, like
"\@1343322750 - 3 days".
-r /path/to/file Show mtime of the given file, not current time.
+"formatstring" Give strftime this format string instead of the
default "%Y-%m-%d %H:%M:%S". See 'man strftime'
-q date.pl warns you when given conflicting
information, i.e. feb 29 not on a leap year.
It also warns you when input isn't understood.
-q suppesses this.
Examples:
date.pl # current time in YYYY-MM-DD HH:MM:SS
TZ="UTC" ./date.pl # Use an alternate time zone
date.pl +"%a %b %d %Y %r" # Like Thu Jan 16 2014 12:58:59 PM
date.pl -d "+ 3 days" # Current time plus three days
date.pl -d "\@1343322750" # exact time in epoch seconds
date.pl -d "2013/01/02 12:00:00"# exact time in YYYYMMDD HHMMSS
date.pl -d "9am" # Today at 9am
date.pl -d "last week + 5 minutes"
date.pl -r /etc/passwd # display mtime of /etc/passwd
date.pl -r /etc/passwd -d "12:00:00" # date of /etc/passwd, time of noon
EOT
exit(1);
}
else { die("unknown argument $arg, try --help"); }
}
# Load hashes full of day and month names
#loadhash(\%month, `locale mon`, `locale abmon`);
loadhash(\%month, "January;February;March;April;May;June;July;August;September;October;November;December",
"Jan;Feb;Mar;Apr;May;Jun;Jul;Aug;Sep;Oct;Nov;Dec");
#loadhash(\%days, `locale day`, `locale abday`);
loadhash(\%days, "Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday",
"Sun;Mon;Tues;Wed;Thurs;Fri;Sat");
@lt=localtime($ref);
# Lowercase input so we can look up tokens like 'monday' in hashes
$input=lc($input);
# Separate strings and numbers into their own tokens, like "9am" => "9 am"
# : still belongs with numbers for HH:MM:SS etc.
$input =~ s/([0-9:])([a-z])/$1 $2/g;
$input =~ s/([a-z])([0-9:])/$1 $2/g;
# Split input on whitespace and commas and jam back into ARGV
unshift(@ARGV, split(/[ \r\n\t,]+/, $input));
while(!$sign && defined($arg=shift))
{
# Need to split +1 into +, 1
if($arg =~ /^[+]/) {
$sign=1;
if(length($arg) > 1) { unshift(@ARGV, substr($arg,1)); }
next;
}
elsif($arg =~ /^-/) {
$sign=-1;
if(length($arg) > 1) { unshift(@ARGV, substr($arg,1)); }
next;
}
if($arg eq "plus") { $sign=1; next; }
if($arg eq "minus") { $sign=-1; next; }
if(length($arg) == 0){ next } # Empty string? Ignore
################## DATE FORMAT DETECTION ########################
# Dates like "Jan" "17"
if(exists($month{$arg}))
{
set(MON, $month{$arg});
if(($#ARGV >= 0) && ($ARGV[0] =~ /^[0-9]+$/))
{
set(DAY, $ARGV[0] + 0);
shift;
}
next;
}
# mon/monday/etc
if(exists($days{$arg})) {
set(WDAY, $days{$arg} );
# If it's followed by a numeral, i.e. Monday 7,
# the numeral is the day of the month
if(($#ARGV >= 0) && ($ARGV[0] =~ /^[0-9]+$/))
{ set(DAY, $ARGV[0] + 0); shift; }
next;
}
# a bare 4-digit number beginning with 19 or 20 is probably a year
if($arg =~ /^(19[0-9][0-9])|^(2[0-9][0-9][0-9])$/)
{ set(YEAR, $arg - 1900); next; }
# @1234 means seconds in epoch time
if($arg =~ /^@([0-9]+)$/) {
@lt=localtime($1+0);
# Date has been replaced, prev changes are now irrelevant
for($n=0; $n<8; $n++) { $changed[$n]=undef; }
next;
}
# Checks for YYYYMMDD or YYYY/MM/DD time
if(($arg =~ /^([0-9]{4})([\/-])([0-9]{1,2})\2([0-9]{1,2})$/) ||
($arg =~ /^([0-9]{4})()([0-9]{2})([0-9]{2})$/))
{
set(YEAR, $1-1900); set(MON, $3-1); set(DAY, $4+0);
# Set time variables which haven't been set already
if(!defined($changed[HOUR]))
{ set(SEC,0); set(MIN,0); set(HOUR,0); }
next;
}
# HH:MM:SS times. Sub-second times are allowed but ignored
if($arg =~ /([0-2]?[0-9]):([0-5][0-9])(:([0-5][0-9])(.[0-9]+)?)?$/)
{
if(defined($4)) { set(SEC, $4+0); }
else { set(SEC, 0); }
set(MIN, $2+0); set(HOUR, $1+0);
# Handle time with PM in it
if($#ARGV < 0) { }
elsif($ARGV[0] eq "pm") { set(HOUR, pm($lt[HOUR])); shift; }
elsif($ARGV[0] eq "am") { set(HOUR, am($lt[HOUR])); shift; }
next;
}
# Times like 9 AM
if(($arg =~ /^([0-9]+)$/) && ($#ARGV>=0) && ($ARGV[0] =~ /^(am|pm)$/)) {
$arg=$arg + 0;
set(SEC, 0); set(MIN,0); set(HOUR, $arg);
if($ARGV[0] eq "am") { set(HOUR, am($lt[HOUR])); }
else { set(HOUR, pm($lt[HOUR])); }
shift;
next;
}
# Redundant, but whatever
if($arg =~ /^now$/) {
@lt=localtime(time);
# Date has been replaced, prev changes are now irrelevant
for($n=0; $n<8; $n++) { $changed[$n]=undef; }
next
}
# last second/minute/hour/week/year
if($arg =~ /^(last)|(next)$/)
{
if(defined($1)) { $offset=-1; } else { $offset=1; }
if($#ARGV < 0)
{
if(!$quiet)
{ print STDERR "next what, exactly?\n" }
next;
}
$ARGV[0] =~ s/s$//;
if(defined($mult{$ARGV[0]}))
{
@lt=localtime(time);
# Date has been replaced, prev changes are now irrelevant
for($n=0; $n<8; $n++) { $changed[$n]=undef; }
$offset *= $mult{$ARGV[0]};
}
elsif($ARGV[0] eq "month")
{
$offset=0;
$lt[MON] += $sign;
if($lt[MON] >= 12) { $lt[YEAR]++; $lt[MON] -= 12; }
if($lt[MON] < 0) { $lt[MON] += 12; $lt[YEAR] --; }
set(MON, $lt[MON]);
set(YEAR, $lt[YEAR]);
}
elsif(!$quiet)
{
print STDERR $ARGV[0]." not a valid option for next\n";
}
shift;
next;
}
if(! $quiet) {
print STDERR "Unknown argument $arg\n";
}
}
# If a + / - sign was found, process the next arguments as the offset
while($sign && defined($arg=shift))
{
# Might find another sign.
if($arg =~ /^[+]/) {
$sign=1;
if(length($arg) > 1) { unshift(@ARGV, substr($arg,1)); }
next;
}
elsif($arg =~ /^-/) {
$sign=-1;
if(length($arg) > 1) { unshift(@ARGV, substr($arg,1)); }
next;
}
if($arg eq "plus") { $sign=1; next; }
if($arg eq "minus") { $sign=-1; next; }
# A number followed by a type, "9" "years"
$arg =~ s/s$//;
if(($arg =~ /^[0-9]+$/) && ($#ARGV >= 0))
{
# second/minute/hour/day/week/year are just multiplication
$ARGV[0] =~ s/s$//g;
if(defined($mult{$ARGV[0]}))
{
$offset += $mult{$ARGV[0]} * $arg * $sign;
shift;
}
elsif($ARGV[0] =~ /months?/) # months means counting
{
while($arg >= 12)
{
$arg -= 12;
$offset += $sign * $mult{"year"};
}
$lt[MON] += $arg * $sign;
while($lt[MON] >= 12) { $lt[MON]-=12; $lt[YEAR]++; }
while($lt[MON] < 0) { $lt[MON]+=12; $lt[YEAR]--; }
set(MON, $lt[MON]); set(YEAR, $lt[YEAR]);
shift;
}
else
{
if(!$quiet) {
print STDERR "Unknown element ".$ARGV[0]."\n";
}
shift;
}
}
}
# Convert the altered year, month, etc back into epoch time.
my $nref=mktime(@lt);
# Convert back into year, month, etc, to see if the input made sense.
my @san=localtime($nref);
for($n=0; $n<=6; $n++)
{
if(($quiet == 0) && defined($changed[$n]) && ($changed[$n] != $san[$n]))
{
printf STDERR "%s changed, inconsistent input?", $title[$n];
printf STDERR "\t%s in %s out\n", $changed[$n], $san[$n];
}
}
# Print the calculated time plus offset
print strftime($cmdstr."\n", localtime($nref + ($offset)));
exit(0);