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 };
# 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);
# Lookup table to convert 'year' into a number of seconds, etc
my %mult=("second" => 1, "seconds" => 1, "minute" => 60, "minutes" => 60,
"hour" => 3600, "hours" => 3600, "day" => 86400, "days" => 86400,
"week" => 604800, "weeks" => 604800,
"year" => 31536000, "years" => 31536000 );
# Parse commandline arguments
while(defined($arg=shift)) {
if($arg =~ /^--date=(.*)/) { $input=$1; }
elsif($arg eq "-d") { $input=shift; }
elsif($arg =~/^\+(.*)/) { $cmdstr=$1; }
elsif($arg eq "-q") { $quiet=1; }
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.1.0, 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 'tues', etc can be reliably found in tables
$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 +/- into their own tokens
$input =~ s/([+-])/ $1 /g;
# Split input on whitespace and commas and jam back into ARGV
unshift(@ARGV, split(/[ \r\n\t,]+/, $input));
while(defined($arg=shift))
{
if(length($arg) == 0){ next; } # Empty string? Ignore
# Handle these in offset section
if(($arg eq "+")||($arg eq "plus")||($arg eq "-")||($arg eq "minus"))
{ unshift(@ARGV, $arg); last; }
################## DATE FORMAT DETECTION ########################
if(exists($month{$arg})) # Dates like "Jan" "17"
{
set(MON, $month{$arg});
if(($#ARGV >= 0) && ($ARGV[0] =~ /^[0-9]+$/))
{ set(DAY, $ARGV[0] + 0); shift; }
next;
}
if(exists($days{$arg})) # mon/monday/etc
{
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)) { $sign=-1; } else { $sign=1; }
if($#ARGV < 0)
{
if(!$quiet)
{ print STDERR "next what, exactly?\n" }
next;
}
if(defined($mult{$ARGV[0]}))
{
@lt=localtime(time);
# Date has been replaced, prev changes now irrelevant
for($n=0; $n<8; $n++) { $changed[$n]=undef; }
$offset *= $mult{$ARGV[0]};
}
# Adding months can't be handled in offset sadly
elsif(($ARGV[0] eq "month") || ($ARGV[0] eq "months"))
{ add_month($sign); }
elsif(!$quiet)
{
print STDERR $ARGV[0]." not a valid option for next\n";
}
shift;
next;
}
if(! $quiet) { print STDERR "Unknown argument $arg\n"; }
}
# If there are any arguments left, we found a +/- and need to process
# that time offset.
while(defined($arg=shift))
{
if(length($arg) == 0){ next; } # Empty string? Ignore
if(($arg eq "plus") || ($arg eq "+")) { $sign=1; next; }
if(($arg eq "minus") ||($arg eq "-")) { $sign=-1; next; }
# A number followed by a type, "9" "years"
if(($arg =~ /^[0-9]+$/) && ($#ARGV >= 0))
{
my $arg2=shift;
if((! $sign)&&(!$quiet))
{
print STDERR "Warning, no sign for numeric value\n"
}
# second/minute/hour/day/week/year are just multiplication
if(defined($mult{$arg2}))
{ $offset += $mult{$arg2} * $arg * $sign; next; }
# No exact number of seconds per month, just count
elsif(($arg2 eq "month")||($arg2 eq "months"))
{ add_month($sign * $arg); next; }
# Leave for error handler below to find
$arg=$arg2;
}
# If we get here, something went wrong.
if(!$quiet) {
print STDERR "Unknown syntax ".$ARGV[0]."\n";
}
}
my $nref=mktime(@lt); # Convert the altered @lt values back into epoch time
# Sanity checking. If localtime(mktime(@lt)) produces different values
# from what was in @lt, we must have given it a nonsensical value which
# mktime corrected.
my @san=localtime($nref);
# Titles for localtime() array elements
my @title=("Seconds","Minutes","Hours","Day","Month","Year","Weekday");
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);
#########################################################################
################################ SUBROUTINES ############################
#########################################################################
# Adds or subtracts a number of months to the time in @lt,
# accounting for year wraparound when the number goes above 11 or below 0
sub add_month {
$lt[MON] += shift;
while($lt[MON] >= 12) { $lt[YEAR]++; $lt[MON] -= 12; }
while($lt[MON] < 0) { $lt[MON] += 12; $lt[YEAR] --; }
set(MON, $lt[MON]);
set(YEAR, $lt[YEAR]);
}
# Alter a value in @lt, and mark that index as 'changed' by altering
# the value in @changed too
sub set { my ($i,$v)=(shift,shift); $lt[$i]=$changed[$i]=$v; }
# Takes a numeric AM hour, returns a number in 24-hour time
sub am { if($_[0] == 12) { return($_[0] - 12); } return($_[0]); }
# Takes a numeric PM hour, 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++; }
}
}