#/bin/sh
#In a recent posting someone was looking for perl routines that
#manipulate dates.  Here's a perl library that implements the standard
#jday and jdate functions (as described in Collected Algorithms of
#the ACM).  There are also routines which return the month name and
#weekday name given a month number of weekday number.  And there are routines
#that return the Julian day number for today, tomorrow and yesterday.
#
#As a bonus prize, you also get an RPN-style date calculator.  Similar to bc,
#it also allows you to push perl expressions onto the stack -- thanks to
#the magic of `eval'.  Moreover, your expression can contain dates (like
#Jan 1, 1991) or functions like `today', `tomorrow' or `yesterday'.
#
#Just cut everything below the cut line and feed it to sh.  You'll get
#date.pl (the subroutine library) and dtc (the calculator).  You'll
#probably want to edit the first line of dtc.
#
#I would have like to have included routines that scan for any date format
#and extract it, but I haven't gotten around to it yet.  Consequently, dtc
#supports only a few date formats.  Sorry, but what's here is useful enough.
#
#Disclaimer:  no warranty is expressed or implied
#
#Right to copy:  you can do anything you want with this (but if you make
#                lots of money from it, send me some)
#
#------------------------ cut line ------------------------------------
#/bin/sh
# This is a shell archive, meaning:
# 1. Remove everything above the #!/bin/sh line.
# 2. Save the resulting text in a file.
# 3. Execute the file with /bin/sh (not csh) to create the files:
#       date.pl
#       dtc
# This archive created: Wed Feb  6 23:45:04 1991
# By:   Gary Puckering ()
export PATH; PATH=/bin:$PATH
if test -f 'date.pl'
then
       echo shar: over-writing existing file "'date.pl'"
fi
cat << \SHAR_EOF > 'date.pl'
package date;

# The following defines the first day that the Gregorian calendar was used
# in the British Empire (Sep 14, 1752).  The previous day was Sep 2, 1752
# by the Julian Calendar.  The year began at March 25th before this date.

$brit_jd = 2361222;

sub main'jdate
# Usage:  ($month,$day,$year,$weekday) = &jdate($julian_day)
{
       local($jd) = @_;
       local($jdate_tmp);
       local($m,$d,$y,$wkday);

       warn("warning:  pre-dates British use of Gregorian calendar\n")
               if ($jd < $brit_jd);

       $wkday = ($jd + 1) % 7;       # calculate weekday (0=Sun,6=Sat)
       $jdate_tmp = $jd - 1721119;
       $y = int((4 * $jdate_tmp - 1)/146097);
       $jdate_tmp = 4 * $jdate_tmp - 1 - 146097 * $y;
       $d = int($jdate_tmp/4);
       $jdate_tmp = int((4 * $d + 3)/1461);
       $d = 4 * $d + 3 - 1461 * $jdate_tmp;
       $d = int(($d + 4)/4);
       $m = int((5 * $d - 3)/153);
       $d = 5 * $d - 3 - 153 * $m;
       $d = int(($d + 5) / 5);
       $y = 100 * $y + $jdate_tmp;
       if($m < 10) {
               $m += 3;
       } else {
               $m -= 9;
               ++$y;
       }
       ($m, $d, $y, $wkday);
}


sub main'jday
# Usage:  $julian_day = &jday($month,$day,$year)
{
       local($m,$d,$y) = @_;
       local($ya,$c);

       $y = (localtime(time))[5] + 1900  if ($y eq '');

       if ($m > 2) {
               $m -= 3;
       } else {
               $m += 9;
               --$y;
       }
       $c = int($y/100);
       $ya = $y - (100 * $c);
       $jd =  int((146097 * $c) / 4) +
                  int((1461 * $ya) / 4) +
                  int((153 * $m + 2) / 5) +
                  $d + 1721119;
       warn("warning:  pre-dates British use of Gregorian calendar\n")
               if ($jd < $brit_jd);
       $jd;
}

sub main'is_jday
{
# Usage:  if (&is_jday($number)) { print "yep - looks like a jday"; }
       local($is_jday) = 0;
       $is_jday = 1 if ($_[0] > 1721119);
}

sub main'monthname
# Usage:  $month_name = &monthname($month_no)
{
       local($n,$m) = @_;
       local(@names) = ('January','February','March','April','May','June',
                        'July','August','September','October','November',
                        'December');
       if ($m ne '') {
               substr($names[$n-1],0,$m);
       } else {
               $names[$n-1];
       }
}

sub main'monthnum
# Usage:  $month_number = &monthnum($month_name)
{
       local($name) = @_;
       local(%names) = (
               'JAN',1,'FEB',2,'MAR',3,'APR',4,'MAY',5,'JUN',6,'JUL',7,'AUG',8,
               'SEP',9,'OCT',10,'NOV',11,'DEC',12);
       $name =~ tr/a-z/A-Z/;
       $name = substr($name,0,3);
       $names{$name};
}

sub main'weekday
# Usage:  $weekday_name = &weekday($weekday_number)
{
       local($wd) = @_;
       ("Sun","Mon","Tue","Wed","Thu","Fri","Sat")[$wd];
}

sub main'today
# Usage:  $today_julian_day = &today()
{
       local(@today) = localtime(time);
       local($d) = $today[3];
       local($m) = $today[4];
       local($y) = $today[5];
       $m += 1;
       $y += 1900;
       &main'jday($m,$d,$y);
}

sub main'yesterday
# Usage:  $yesterday_julian_day = &yesterday()
{
       &main'today() - 1;
}

sub main'tomorrow
# Usage:  $tomorrow_julian_day = &tomorrow()
{
       &main'today() + 1;
}

SHAR_EOF
if test -f 'dtc'
then
       echo shar: over-writing existing file "'dtc'"
fi
cat << \SHAR_EOF > 'dtc'
#/usr/local/bin/perl -I/home/garyp/perl

require 'date.pl';

$command = '';
print " Date Calculator version 1.0\n";
print "    (type `h' for help)\n";
print "> ";

while(<stdin>) {
       ($command) = /^\s*(\w+)\s*$/;
       last if (index("quit",$command) == 0);
       if (/^\s*(\d+)\s+(\d+)\s+(\d+)\s*$/) {                  # quit
               $j = &jday($1,$2,$3);
               push(@stack,$j);
               next;
       }
       elsif (/^\s*(\w+)\s+(\d+)(\s+(\d+)?)\s*$/) {    # mmm dd yy
               # assumes this year if year is missing
               $j = &jday(&monthnum($1),$2,$4);
               push(@stack,$j);
               next;
       }
       elsif (/^\s*([-]?\d+)\s*$/) {                                   # [-]n
               push(@stack,$1);
               next;
       }
       elsif (index("clear",$command)==0) {                    # clear
               @stack = ();
               next;
       }
       elsif (index("duplicate",$command)==0) {                # duplicate
               push(@stack,$stack[$#stack]);
               next;
       }
       elsif (index("exchange",$command)==0 ||
              $command eq 'x') {                                               # exchange
               $x = pop(@stack);
               $y = pop(@stack);
               push(@stack,$x);
               push(@stack,$y);
               next;
       }
       elsif (index("print",$command)==0) {                    # print
               do print($stack[$#stack]);
               next;
       }
       elsif (index("today",$command)==0) {                    # today
               push(@stack,&today());
               do print($stack[$#stack]);
               next;
       }
       elsif (/^\s*[+]\s*$/) {                                                 # add
               $y = pop(@stack);
               $x = pop(@stack);
               if (&is_jday($x) && &is_jday($y)) {
                       print stderr "** cannot add two dates\n";
                       push(@stack,$x);
                       push(@stack,$y);
                       next;
               }
               $r = $x + $y;
               push(@stack,$r);
               do print($r);
               next;
       }
       elsif (m:^\s*([\-*/%])\s*$:) {                                  # (-) (*) (/) and (%)
               $y = pop(@stack);
               $x = pop(@stack);
               $r = eval "$x $+ $y";
               warn "** evaluation error $@\n" if $@ ne "";
               push(@stack,$r);
               do print($r);
               next;
       }
       elsif (index("Print",$command)==0) {                            # dump
               do dump();
               next;
       }
       elsif (index("help",$command)==0) {                                     # help
               print <<EOD ;
Commands:

       mmm dd          Push date for current year onto stack
       mmm dd yyyy     Push date onto stack
       n or -n         Push positive/negative constant or interval onto stack
       + - * / %       Add, subtract, multiply, divide, modulo
       expr            Push result of Perl expression onto stack
       <d>uplicate     Push a duplicate of the top value onto the stack
       <c>lear         Clear stack
       <p>rint         Print last value on stack
       <P>rint         Print all stack values
       <t>oday         Put today's date on the stack
       e<x>change      Exchange top two values of stack
       <q>uit          Exit the program

Note:   expressions are scanned for embedded dates of the form `1991/Jan/2',
       `Jan 1, 1991' or just `Jan 1'.  These dates are translated to Julian
       Day numbers before the expression is evaluated.  Also, the tokens
       `today', `tomorrow' and `yesterday' are replaced with their
       respective Julian Day numbers.  If the expression does something
       stupid with Julian Day numbers (like add them) you get silly
       results.
EOD
               next;
       }
       else {
               chop;
               # replace yyyy/mmm/dd dates with Julian day number
                 s|(\d{1,4})\W?(\w\w\w)\W?(\d\d?)|&jday(&monthnum($2),$3,$1)|ge;
               # replace mmm dd yyyy dates with Julian day number
                 s|(\w\w\w)[\W\s](\d\d?)[,]?[\W\s](\d{1,4})|&jday(&monthnum($1),$2,$3)|ge;
               # replace mmm dd dates with Julian day number (for this year)
                 s|(\w\w\w)[\W\s](\d\d?)|&jday(&monthnum($1),$2)|ge;
               # replace 'today' with todays jday
                 s|\b(today)\b|&today()|ge;
               # replace 'tomorrow' with tomorrows jday
                 s|\b(tomorrow)\b|&tomorrow()|ge;
               # replace 'yesterday' with yesterdays jday
                 s|\b(yesterday)\b|&yesterday()|ge;
               print $_,"\n";
               push(@stack,eval($_));
               do print($stack[$#stack]);
               next;
       }
#       else { warn "** invalid command - try \"help\"\n" unless ($_ eq "\n"); }
} continue {
       print "> ";
       $command = "";
}

sub print #(value)
{
       if (&is_jday($_[0])) {
               ($m,$d,$y,$wd) = &jdate($_[0]);
               $month = &monthname($m,3);
               $wkday = &weekday($wd);
               print "= $wkday $month $d, $y (JD = $_[0])\n";
       } else {
               if ($_[0] > 365 || $_[0] < -365) {
                       $years = int($_[0] / 365.25);
                       $days = $_[0] - int($years * 365.25);
                       print "= $_[0] days  ($years years, $days days)\n\n";
               } else {
                       print "= $_[0] days\n\n";
               }
       }
}

sub dump
{
       for ($i = 0; $i <= $#stack; $i++) {
               print "stack[",$i,"] ";
               do print($stack[$i]);
       }
}
SHAR_EOF
chmod +x 'dtc'
#       End of shell archive
exit 0
--
Gary Puckering                             Cognos Incorporated
 VOICE: (613) 738-1338 x6100              P.O. Box 9707
 UUCP:  uunet!mitel!cunews!cognos!garyp   Ottawa, Ontario
 INET:  garyp%[email protected]    CANADA  K1G 3Z4