#/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%cognos.uucp@uunet.uu.net    CANADA  K1G 3Z4

