Path: utzoo!utgpu!jarvis.csri.toronto.edu!mailrus!wasatch!cs.utexas.edu!uunet!allbery
From: allbery@uunet.UU.NET (Brandon S. Allbery - comp.sources.misc)
Newsgroups: comp.sources.misc
Subject: v08i011: moon(1) -- another phase-of-the-moon-program
Message-ID: <64306@uunet.UU.NET>
Date: 20 Aug 89 00:53:40 GMT
Sender: allbery@uunet.UU.NET
Reply-To: ajs@hpfcajs.hp.com (Alan Silverstein)
Lines: 4859
Approved: allbery@uunet.UU.NET (Brandon S. Allbery - comp.sources.misc)

Posting-number: Volume 8, Issue 11
Submitted-by: ajs@hpfcajs.hp.com (Alan Silverstein)
Archive-name: moon

[Alan asked me if it was an imposition to submit this.  Oy veh.  ++bsa]

OK, here you go.  Note that the sharchive includes other stuff you
need to build moon.c (parsedate), and stuff you might want to run
the accuracy script (anod, stddev).  I'll let you decide if it's all
worth shipping or not.

Major features:  clean source code; lots of options; includes drawing
an ASCII picture (also can print phase as a number for feeding to a true
graphics program); very accurate; manual entry; test scripts.

Alan Silverstein


# This is a shell archive.  Remove anything before this line,
# then unpack it by saving it in a file and typing "sh file".
#
# Wrapped by ajs at hpfcajs on Thu Aug 10 17:44:21 1989
#
# This archive contains:
#	misc		moon		parsedate	
#
# Error checking via wc(1) will be performed.

echo mkdir - misc
mkdir misc

echo x - misc/anod.1
sed 's/^@//' >misc/anod.1 <<'@EOF'
.TH ANOD 1 "Unsupported Utility, Version 4"
.SH NAME
anod \- add number of days to dates/values
.SH SYNOPSIS
.B anod
[
.B \-ipwWmM
] [
.BI \-c \0date
] [
.B \-C
] [
.I files...
]
.ad b
.SH DESCRIPTION
This program adds a ``number of days'' (or weeks or months) column to data
whose first field is a date in the form ``YYMM[DD]''
(or optionally including a time).
It is useful for quick interactive inquiries of amounts of time between dates,
accurately plotting data points gathered on non-periodic dates,
and converting occurrence dates to interval sizes (e.g. for scattergramming).
.PP
The program reads the concatenation of named
.I files
(use ``\-'' for standard input),
or reads standard input if no filenames are given,
and writes results to standard output.
.PP
Input lines which are comments (first field starts with ``#'') or blank lines
are passed through to standard output unaltered.
The first field of each data line must be a four or six digit date
(YYMM or YYMMDD) in the 20th century.
If no day of month is present, ``01'' is assumed.
.PP
Output lines have one blank and a number appended after the first (date) field:
the number of days before or after the date given on the first data line
(always zero for the first data line).
.PP
Options are:
.TP
.B \-i
Intervals:  On each data line,
add (insert) the delta from the date on the previous data line,
not from the first date.
.TP
.B \-p
Higher precision:
Each data line can contain a time after the date, in this form:
YYMM[DD[.[HH[MM[SS]]]]].
If any part of the time (hours, minutes, or seconds) is missing,
``00'' is assumed.
This option is allowed when adding days or weeks only.
When adding days, fractional days are printed when appropriate.
.TP
.B \-w
Add the number of whole (seven-day) weeks, not days, since the first date.
Fractions of weeks are ignored.
.TP
.B \-W
Like
.BR \-w ,
but add exact (fractional) weeks, not whole weeks.
.TP
.B \-m
Add the number of whole calendar months, not days, since the first date.
A month is the amount of time between successive same days-of-month,
e.g. between 850812 and 850912 is one month.
Fractions of months are ignored.
.TP
.B \-M
Like
.BR \-m ,
but add approximate (fractional) months, not whole months.
The fractional part is the number of excess days divided by the average days
per month (365.25 / 12).
.TP
.BI \-c \0date
Convert the matching date,
if it appears in the input data,
to a dot (``.'') in output.
In other words, emit a dot instead of repeating the date field as given,
with the rest of the line the same (including the added number of days).
.IP
This is intended for use with
.IR dataplot (1),
which understands a dot to mean ``don't plot this label''.
Give as many
.B -c
options as you need,
one per date to convert.
If
.I date
is of the form YYMM,
DD is assumed to be 01,
e.g. 8605 matches 860501.
.TP
.B \-C
Convert all input dates to ``.'' in output.
Giving any
.B \-c
options is redundant.
The same results could be gained by piping the data through
.IR sed (1)
or
.IR awk (1),
but this is more convenient.
.PP
You can only give one of
.BR \-w ,
.BR \-W ,
.BR \-m ,
or
.BR \-M .
.PP
The program doesn't worry about aligning its output,
which normally is passed directly to another program, e.g.
.IR dataplot (1).
.SH EXAMPLES
.TP
anod data1 > data1.plot
Add day intervals since the first date to dates in file ``data1''
and save results in ``data1.plot''.
.TP
anod -im -c861214 -c 8701 < t
Add number-of-months column to data in file ``t'',
where each number of months is incremental from the previous date,
and convert dates 861214 and 870101 to ``.'' in the output.
.SH SEE ALSO
bars(1), dataplot(1)
.SH LIMITATIONS
Dates input are restricted to format YYMM[DD].
However, most ``rational'' date formats can be converted to/from this style
with little trouble.
.SH DIAGNOSTICS
Prints a message standard error and exits
if invoked wrong or it detects invalid-format data.
@EOF
if test "`wc -lwc misc/anod.c <<'@EOF'
static char *version = "@(#) 4 880804";
/*
 * Add number of days (Julian days from start date) to a list of dates.
 * See the manual entry (anod(1)) for details.
 *
 * Compile with -DDEBUG to test JulianDate().
 */

#include 
#include 


/*********************************************************************
 * MISCELLANEOUS GLOBAL VALUES:
 */

#define	PROC				/* null; easy to find procs */
#define	FALSE	0
#define	TRUE	1
#define	CHNULL	('\0')
#define	CPNULL	((char *) NULL)
#define	REG	register

char *usage[] = {
    "usage: %s [-iwWmM] [-c date] [-C] [files...]",
    "-i intervals: show each delta from previous date, not from first date",
    "-p higher precision: input has times of days; add fractional days",
    "-w add number of whole (seven-day) weeks, not days, since the first date",
    "-W like -w, but add exact (fractional) weeks",
    "-m add number of whole calendar months, not days, since the first date",
    "-M like -m, but add approximate (fractional) months",
    "-c convert matching date to \".\" in output",
    "-C convert all dates to \".\" in output",
    "First fields of non-comment input lines must be dates in the form",
    "YYMM[DD].  If DD is missing, 01 is assumed.  If -p, dates can be",
    "followed by times: .HH[MM[SS]]] (default .000000).  Number fields are",
    "inserted after the date field.",
    CPNULL,
};

char	*defargv[] = { "-" };		/* default argument list	*/

char	*myname;			/* how program was invoked	*/
int	iflag = FALSE;			/* -i (intervals) option	*/
int	pflag = FALSE;			/* -p (precision) option	*/
int	wflag = FALSE;			/* -w (weeks, whole) option	*/
int	Wflag = FALSE;			/* -W (weeks, exact) option	*/
int	mflag = FALSE;			/* -m (months, whole) option	*/
int	Mflag = FALSE;			/* -M (months, exact) option	*/
int	Cflag = FALSE;			/* -C (convert all dates) opt	*/

#define	MAXCONV	500			/* maximum dates to convert	*/
#define	DOT	'.'			/* what to convert dates to	*/
double	convdate [MAXCONV];		/* Julian dates from -c options	*/
int	convcount = 0;			/* number of -c dates given	*/

double	prevdate = 0;			/* Julian of previous date	*/

int	prevyear, prevmonth, prevday;	/* parts  of previous date	*/
int	year,	  month,     day;	/* parts  of current  date	*/
int	length;				/* length of current  date	*/

#define	SHORTLEN  4			/* length of short date (YYMM)	*/
#define	DATELEN   6			/* length of normal date	*/
#define	HHLEN	  2			/* length of hour only		*/
#define	HHMMLEN	  4			/* length of hour plus minutes	*/
#define	HHMMSSLEN 6			/* length of complete time	*/

/*
 * Days in each month (February is special) and in year before each month:
 *
 *		    0  Jan Feb Mar  Apr May Jun  Jul Aug Sep  Oct Nov Dec */
int monthdays[] = { 0,  31, 28, 31,  30, 31, 30,  31, 31, 30,  31, 30, 31 };
int priordays[] = { 0,   0, 31, 59,  90,120,151, 181,212,243, 273,304,334 };

#define	DAYSperMONTH (365.25 / 12)	/* for -M conversions		*/

double	JulianDate();
double	ParseTime();


/************************************************************************
 * M A I N
 */

PROC main (argc, argv)
	int	argc;
	char	**argv;
{
extern	int	optind;			/* from getopt()	*/
extern	char	*optarg;		/* from getopt()	*/
REG	int	option;			/* option "letter"	*/

REG	FILE	*filep;			/* open input file	*/
	char	*filename;		/* name to use		*/

/*
 * PARSE ARGUMENTS:
 */

	myname = *argv;

	while ((option = getopt (argc, argv, "ipwWmMc:C")) != EOF)
	{
	    switch (option)
	    {
	    case 'i':	iflag = TRUE;	break;
	    case 'p':	pflag = TRUE;	break;
	    case 'w':	wflag = TRUE;	break;
	    case 'W':	Wflag = TRUE;	break;
	    case 'm':	mflag = TRUE;	break;
	    case 'M':	Mflag = TRUE;	break;
	    case 'C':	Cflag = TRUE;	break;

	    case 'c':	if (convcount >= MAXCONV)
			{
			    Error ("can't handle more than %d -c options",
				    MAXCONV);
			}

			if ((convdate [convcount++] = JulianDate (optarg)) < 0)
			{
			    Error ("invalid date with -c option: \"%s\"",
				   optarg);
			}

			break;

	    default:	Usage();
	    }
	}

	argc -= optind;			/* skip options	*/
	argv += optind;

/*
 * MORE ARGUMENT CHECKS:
 */

	if (wflag + Wflag + mflag + Mflag > TRUE)
	    Error ("you can only give one of -w, -W, -m, or -M");

	if (pflag && (mflag || Mflag))
	    Error ("-p is not allowed with -m or -M");
	    /* the math is too hard and result meaningless; see PrintLine() */

/*
 * READ FROM LIST OF FILES OR STDIN:
 */

	if (argc < 1)				/* no file names */
	{
	    argc = 1;
	    argv = defargv;			/* use default */
	}

	while (argc-- > 0)
	{
	    if (strcmp (*argv, "-") == 0)	/* read stdin */
	    {
		filename = "";
		filep	 = stdin;
	    }
	    else if ((filep = fopen ((filename = *argv), "r")) == (FILE *) NULL)
		Error ("can't open file \"%s\" to read it", filename);

	    ReadFile (filename, filep);

	    if (ferror (filep))
		Error ("read failed from file \"%s\"", filename);

	    if (filep != stdin)
		fclose (filep);			/* assume it works */

	    argv++;
	}

	exit (0);

} /* main */


/************************************************************************
 * R E A D   F I L E
 *
 * Given a filename and stream pointer for reading, read lines from the file,
 * skip comment or blank lines, expect dates as first fields of other lines,
 * and insert number-of-days fields.  Error out if an invalid date appears in a
 * data line.
 */

PROC ReadFile (filename, filep)
	char	*filename;
REG	FILE	*filep;
{
REG	int	linenum = 0;		/* input line number	*/
#define	LINESIZE  500
	char	line [LINESIZE];	/* read from file	*/
REG	char	*cp;			/* place in line	*/
REG	double	date;			/* Julian date		*/

/*
 * READ LINE, REMOVE NEWLINE, SKIP LEADING WHITESPACE:
 */

	while ((cp = fgets (line, LINESIZE, filep)) != CPNULL)
	{
	    linenum++;

	    while ((*cp != '\n') && (*cp != CHNULL))
		cp++;				/* look for end of line */

	    *cp = CHNULL;
	    cp  = line;

	    while ((*cp == ' ') || (*cp == '\t'))
		cp++;				/* skip leading whitespace */

/*
 * HANDLE COMMENT, BLANK, OR DATA LINE:
 */

	    if ((*cp == '#') || (*cp == CHNULL))
		puts (line);
	    else
	    {
		if ((date = JulianDate (cp)) < 0)
		{
		    Error ("file \"%s\", line %d: invalid date:\n%s",
			   filename, linenum, line);
		}

		PrintLine (line, cp, date);
	    }
	} /* while */

} /* ReadFile */


/************************************************************************
 * J U L I A N   D A T E
 *
 * Given a string supposed to start with a date (format YYMM[DD], or if pflag,
 * YYMM[DD[.[HH[MM[SS]]]]]), and global monthdays[], check for a valid date
 * (which must be terminated by CHNULL, blank, or tab) and return its Julian
 * equivalent (where date 000101 == day 1.0).  In case of invalid date, return
 * a negative value.
 * 
 * As a side-effect, use and set globals year, month, day, and length, so
 * they're remembered and available if needed by callers.  Only valid if
 * successful.
 */

PROC double JulianDate (datestr)
	char	*datestr;		/* value to check */
{
	char	*endstr;		/* end of datestr */
	long	strtol();
REG	long	date = strtol (datestr, & endstr, 10);
	int	havetime;		/* have time too?    */
	double	time = 0.0;		/* fraction of day   */
	int	leapyear;		/* is it a leapyear? */

/*
 * CHECK FOR SHORT DATE (NO "DD"):
 */

	if ((length = (endstr - datestr)) == SHORTLEN)
	    date = (date * 100) + 1;		/* assume missing DD == 01 */

/*
 * CHECK FOR AND RETURN VALID DATE [AND TIME]:
 *
 * Hope to get through all checks to the "return" statement.
 * Any failure drops out to below.
 */

	havetime = pflag && (*endstr == '.');

	if (((length  == SHORTLEN)
	  || (length  == DATELEN))	/* OK length */
	 && ((*endstr == CHNULL)
	  || (*endstr == ' ')
	  || (*endstr == '\t')
	  || havetime))			/* OK end */
	{
	    year  = date / 10000;
	    month = date / 100 % 100;
	    day	  = date % 100;

	    leapyear = (year > 0) && ((year % 4) == 0);

	    if ((month >= 1) && (month <= 12) && (day >= 1)
	     && (day <= monthdays [month] + (leapyear && (month == 2)))
	     && ((! havetime)
		 || ((time = ParseTime (datestr + (++length))) >= 0)))
	    {
		return ((year * 365)			/* previous years */
			+ (priordays [month])		/* before month	  */
			+ day				/* in this month  */
			+ (year / 4)			/* prior leapdays */
			- (leapyear && (month <= 2))	/* before Feb 29  */
			+ time);			/* return double  */
	    }
	} /* if */

/*
 * HANDLE ERROR:
 */

	return (-1.0);

} /* JulianDate */


/************************************************************************
 * P A R S E   T I M E
 *
 * Given a string supposed to start with a time (format [HH[MM[SS]]]), check
 * for a valid time (which must be terminated by CHNULL, blank, or tab) and
 * return its value as a fraction of 24 hours.  In case of invalid time, return
 * a negative value.
 * 
 * As a side-effect, add to global length the length of the time string.  Only
 * valid if successful.
 */

PROC double ParseTime (timestr)
	char	*timestr;		/* value to check */
{
	char	*endstr;		/* end of datestr */
	long	strtol();
REG	long	time = strtol (timestr, & endstr, 10);
	int	timelen;		/* length of time */
	int	hour, minute, second;	/* parts of time  */

/*
 * CHECK FOR SHORT TIME (NOT HHMMSS):
 */

	if (timestr == endstr)			/* no time value */
	    return (0.0);

	if	((timelen = (endstr - timestr)) == HHLEN)	time *= 10000;
	else if	(timelen == HHMMLEN)				time *= 100;

/*
 * CHECK FOR AND RETURN VALID TIME:
 *
 * Hope to get through all checks to the "return" statement.
 * Any failure drops out to below.
 */

	if (((timelen == HHLEN)
	  || (timelen == HHMMLEN)
	  || (timelen == HHMMSSLEN))		/* OK length */
	 && ((*endstr == CHNULL)
	  || (*endstr == ' ')
	  || (*endstr == '\t')))		/* OK end */
	{
	    length += timelen;

	    hour   = time / 10000;
	    minute = time / 100 % 100;
	    second = time % 100;

	    if ((hour <= 23) && (minute <= 59) && (second <= 59))
		return (((((second / 60.0) + minute) / 60) + hour) / 24);

	} /* if */

/*
 * HANDLE ERROR:
 */

	return (-1.0);

} /* ParseTime */


/************************************************************************
 * P R I N T   L I N E
 *
 * Given an input line, a pointer to a valid date string in the line, the
 * Julian number for that date, and globals convcount, prevdate, prevyear,
 * prevmonth, prevday, year, month, day, length, and monthdays[], print the
 * line with the number-of-days (or weeks or months) inserted after the date.
 * Also update prevdate (and optionally prevyear, prevmonth, and prevday) if
 * this is the first data line (currently prevdate == 0.0) or if iflag is set.
 */

PROC PrintLine (cp, datestr, date)
REG	char	*cp;		/* place in line, initially start */
REG	char	*datestr;	/* start of date in line	  */
	double	date;		/* Julian value of input date	  */
{
REG	int	cflag = Cflag || (convcount && IsConv (date));

#ifdef DEBUG
	printf ("%g\n", date);
	return;
#endif

/*
 * PRINT INDENTATION PART:
 */

	while (cp < datestr)
	{
	    putchar (*cp);
	    cp++;		/* because putchar() is a macro */
	}

/*
 * PRINT DATE STRING, POSSIBLY CONVERTED TO DOT:
 */

	if (cflag)
	{
	    putchar (DOT);
	    cp += length;
	}
	else
	{
	    for (datestr += length; cp < datestr; cp++)
		putchar (*cp);
	}

	/* now cp is at the next char after the date input */

/*
 * PRINT INITIAL "ADDED" VALUE:
 */

	if (prevdate == 0.0)			/* no previous date yet */
	    printf (" 0");

/*
 * PRINT "ADDED" NUMBER OF MONTHS:
 *
 * This computation compares previous and current year, day, and month, rather
 * than prevdate and date, in order to account for months varying in size.
 */

	else if (mflag || Mflag)
	{
	    long interval = ((year - prevyear) * 12) + (month - prevmonth);

	    if (mflag)				/* whole months */
	    {
		/* reduce for incomplete month: */

		if	((interval > 0) && (prevday > day))	interval--;
		else if	((interval < 0) && (prevday < day))	interval++;

		printf (" %ld", interval);
	    }
	    else				/* fractional months */
	    {
		/* not precise, but close enough: */

		printf (" %g", interval + ((day - prevday) / DAYSperMONTH));
	    }
	}

/*
 * PRINT "ADDED" NUMBER OF WEEKS OR DAYS:
 */

	else
	{
	    double interval = date - prevdate;

	    if	    (Wflag)	printf (" %g",  interval / 7);
	    else if (wflag)	printf (" %ld", ((long) interval) / 7);
	    else if (pflag)	printf (" %g",  interval);
	    else		printf (" %ld", (long) interval);
	}

/*
 * PRINT REST OF INPUT LINE; SAVE VALUES:
 */

	puts (cp);

	if (iflag || (prevdate == 0.0))	/* always, or first input line */
	{
	    prevdate = date;		/* save this date as previous */

	    if (mflag || Mflag)		/* need to save them */
	    {
		prevyear  = year;
		prevmonth = month;
		prevday	  = day;
	    }
	}

} /* PrintLine */


/************************************************************************
 * I S   C O N V
 *
 * Given a Julian date and globals convdate[] and convcount, return TRUE if
 * the given date is in convdate[], FALSE otherwise.
 */

PROC int IsConv (date)
REG	double	date;
{
REG	int	index;

	for (index = 0; index < convcount; index++)
	    if (convdate [index] == date)
		return (TRUE);

	return (FALSE);

} /* IsConv */


/************************************************************************
 * U S A G E
 *
 * Print usage messages (char *usage[]) to stderr and exit nonzero.
 * Each message is followed by a newline.
 */

PROC Usage()
{
REG	int	which = 0;		/* current line */

	while (usage [which] != CPNULL)
	{
	    fprintf (stderr, usage [which++], myname);
	    putc ('\n', stderr);
	}

	exit (1);

} /* Usage */


/************************************************************************
 * E R R O R
 *
 * Print an error message to stderr and exit nonzero.  Message is preceded
 * by ": " using global char *myname, and followed by a newline.
 */

/* VARARGS */
PROC Error (message, arg1, arg2, arg3, arg4)
	char	*message;
	long	arg1, arg2, arg3, arg4;
{
	fprintf (stderr, "%s: ", myname);
	fprintf (stderr, message, arg1, arg2, arg3, arg4);
	putc ('\n', stderr);

	exit (1);

} /* Error */
@EOF
if test "`wc -lwc misc/stddev <<'@EOF'
#! /bin/ksh
# Compute standard deviation.

# Usage: