There must be thousands of one-off solutions scattered around this forum. GNU Date is so handy because it's general but if they're asking they probably don't have it. We have some nice scripts but they tend to need dates formatted in a very particular way.
This is a rough approximation which can handle -d "something - 3 days"
kind of requests. It is not nearly so robust as GNU Date.
I'm trying to avoid needing nonstandard modules. Comments, criticism?
#!/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++; }
}
}