[alt.sources] Return of the Son of Yet Another Pcal Revision

rogers@sud509.ed.ray.com (Andrew Rogers) (07/24/90)

This is - what else - another revision to the nefarious Pcal program.  I
have rewritten pcal.c - partially to fix some minor bugs, but mostly to
improve maintainability.  I've also included a better calendar/CALENDAR.DAT
file in response to complaints that the expected format is not documented
very well.

Since the above two files are the only ones that are new, I've elected to
post just those files instead of the complete 'shar' file.

Andrew

PS: My apologies if you've seen this before - an earlier posting appears
to have been garbled.

-------------------------------- cut here --------------------------------
#! /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:
#	 pcal.c
#	 calendar
# This archive created: Mon Jul 23 13:13:06 EDT 1990
export PATH; PATH=/bin:/usr/bin:$PATH
if test -f 'pcal.c'
then
	echo shar: will not over-write existing file 'pcal.c'
else
cat << \SHAR_EOF > 'pcal.c'
/*
 * pcal.c - generate PostScript file to print calendar for any month and year
 *
 * The original PostScript code to generate the calendars was written by
 * Patrick Wood (Copywrite (c) 1987 by Patrick Wood of Pipeline Associates,
 * Inc.), and authorized for modification and redistribution.  The calendar
 * file inclusion code was originally written in "bs(1)" by Bill Vogel of
 * AT&T.  Patrick's original PostScript was modified and enhanced several
 * times by others whose names have regrettably been lost.  This C version
 * was originally created by Ken Keirnan of Pacific Bell; additional
 * enhancements by Joseph P. Larson, Ed Hand, and Andrew Rogers (who also
 * did the VMS port).
 *
 *	Parameters:
 *
 *		pcal [opts]		generate calendar for current month/year
 *
 *		pcal [opts] yy		generate calendar for entire year yy
 *
 *		pcal [opts] mm yy	generate calendar for month mm
 *					(Jan = 1), year yy (19yy if yy < 100)
 *
 *		pcal [opts] mm yy n	as above, for n consecutive months
 *
 *	Output:
 *
 *		PostScript file to print calendars for all selected months.
 *
 *	Options:
 *
 *		-d <FONT>	specify alternate font for day names
 *				(default: Times-Bold)
 *
 *		-e		generate empty calendar (ignore date file)
 *
 *		-f <FILE>	specify alternate date file (default:
 *				~/calendar on Un*x, SYS$LOGIN:CALENDAR.DAT
 *				on VMS)
 *
 *		-o <FILE>	specify alternate output file (default:
 *				stdout on Un*x, CALENDAR.PS on VMS)
 *
 *		-r		generate portrait-style calendars
 *				(default: landscape)
 *
 *		-s		print Saturdays in black
 *		-S		print Saturdays and Sundays in black
 *				(default: print Saturdays and Sundays in gray)
 *		
 *		-t <FONT>	specify alternate font for titles
 *				(default: Times-Bold)
 *
 *		Parameters and flags may be mixed on the command line.
 */


#include <stdio.h>
#include <ctype.h>
#include <time.h>
#include <string.h>

#ifdef VMS		/* VMS oddities isolated here */

#define INFILE  "CALENDAR.DAT"
#define OUTFILE "CALENDAR.PS"
#define S_OPT "\"S\""
#define END_PATH ']'
#define EXIT_SUCCESS 1
#define EXIT_FAILURE 3

#else			/* non-VMS - assume Un*x of some sort */

#define INFILE  "calendar"
#define OUTFILE NULL
#define S_OPT "S"
#define END_PATH '/'
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1
extern char *getenv();	/* to translate "HOME" to path name */

#endif


#define DAYFONT   "Times-Bold"
#define TITLEFONT "Times-Bold"

#define FALSE	0
#define TRUE	1

#define PRT	(void)printf
#define FPR	(void)fprintf

#define is_leap(y) ((y) % 4 == 0 && ((y) % 100 != 0 || (y) % 400 == 0))

#define MIN_YR	1900		/* significant years (calendar limits) */
#define MAX_YR	9999

#define JAN	 1		/* significant months */
#define FEB	 2
#define DEC	12

#define MAXARGS 3		/* numeric command-line args */
#define HOLIDAY	(1 << 6)	/* bit set to flag day as holiday */

#define WHITESPACE " \t"	/* token delimiters in date file */

#include "pcalinit.h"		/* PostScript boilerplate */

/*
 * Global variables:
 */

FILE *dfp = NULL;		/* date file pointer */
int init_year;			/* initial default year for date file entries */
int curr_year;			/* current default year for date file entries */
char *words[100];		/* maximum number of words per date file line */
char lbuf[512];			/* maximum date file line size */


/*
 * Main program - parse and validate command-line arguments, open files,
 * generate PostScript boilerplate and code to generate calendars.
 *
 * Program structure:
 *
 * main() generates the common PostScript code and then calls pmonth() to
 * print the calendars.
 *
 * pmonth() calls find_holidays() to make a first pass through the date file
 * to generate the list of holidays to be printed in gray; it then calls
 * find_daytext() to make a second pass to generate the text to be printed
 * inside the calendar boxes.
 *
 * find_holidays() and find_daytext() both call getday() to retrieve the next
 * day of interest from the date file.
 *
 * getday() reads successive lines from the date file (stripping comments and
 * ignoring blank lines), stopping when parse() determines that the line
 * contains a usable date or when EOF is reached.
 *
 * parse() parses a line from the date file, determining whether or not it
 * contains a date in the current month and year.  It calls utility routines
 * is_valid() to validate the date found and loadwords() to split any
 * accompanying text into individual tokens for PostScript to print.
 * 
 */
main(argc, argv)
	int argc;
	char **argv;
{

/* Look for the argument following a flag - may be separated by spaces or
 * not.  If no non-flag argument appears, leave "arg" alone
 */
#define GETARG(arg) if ((parg = *++opt ? opt : \
			(*(argv+1) && **(argv+1) != '-' ? *++argv : NULL) ) \
			!= NULL) arg = parg; else

/* Loop through one of the header sections in pcalinit.h */
#define DOHEADER(phdr) for (ap = phdr; *ap; ap++) PRT("%s\n", *ap)

	char *progname = **argv ? *argv : "pcal";
	struct tm *p_tm;
	register char **ap;
	char *date_file = NULL;
	char *opt, *parg, *p;
	long tmp;
	int nocal = FALSE;
	int sat_gray = TRUE;
	int sun_gray = TRUE;
 	char *titlefont = TITLEFONT;
 	char *dayfont = DAYFONT;
	char *outfile = OUTFILE;
 	int rotate = 90;
 	int month, year, nmonths;
	int badopt = FALSE;		/* flag set if bad option  */
	int badpar = FALSE;		/* flag set if bad param   */
	int default_out = TRUE;		/* flag if default output file (VMS) */
	int nargs = 0;			/* count of non-flag args  */
	int numargs[MAXARGS];		/* non-flag (numeric) args */

	/* isolate root program name (for use in error messages) */

	if ((p = strrchr(progname, END_PATH)) != NULL)
		progname = ++p;
	if ((p = strchr(progname, '.')) != NULL)
		*p = '\0';

	/* walk through command-line arguments */

 	while (*++argv) {

		if (**argv != '-') {	/* assume numeric argument */
		    	if (nargs < MAXARGS)
				numargs[nargs++] = atoi(*argv);
			continue;
			}

		opt = (*argv) + 1;
		switch (*opt) {

 		case 'd':		/* specify alternate day font */
 			GETARG(dayfont);
 			break;

		case 'e':		/* generate empty calendar */
			nocal = TRUE;
			date_file = NULL;
			break;

		case 'f':		/* specify alternate date file */
			GETARG(date_file);
			nocal = FALSE;
			break;

		case 'o':		/* specify alternate output file */
			GETARG(outfile);
			default_out = FALSE;
			break;

 		case 'r':		/* generate portrait calendar */
 			rotate = 0;
 			break;
 
		case 'S':		/* Saturdays and Sundays in black */
			sun_gray = FALSE;
		case 's':		/* Saturdays in black */
			sat_gray = FALSE;
			break;

 		case 't':		/* specify alternate title font */
 			GETARG(titlefont);
 			break;

		default:
			FPR(stderr, "%s: illegal option -%s\n", progname, opt);
			badopt = TRUE;
			break;
		}
        }

	/* Get and validate non-flag (numeric) parameters */

	switch (nargs) {
	case 0:		/* no arguments - print current month/year */
		time(&tmp);
		p_tm = localtime(&tmp);
		month = p_tm->tm_mon + 1;
		year = p_tm->tm_year;
		nmonths = 1;
		break;			
	case 1:		/* one argument - print entire year */
		month = 1;
		year = numargs[0];
		nmonths = 12;
		break;
	default:	/* two or three arguments - print one or more months */
		month = numargs[0];
		year = numargs[1];
		nmonths = nargs > 2 ? numargs[2] : 1;
		break;
	}

	if (year > 0 && year < 100)	/* treat nn as 19nn */
		year += 1900;
	
	if (nmonths < 1)		/* ensure at least one month */
		nmonths = 1;
	
	if (month < 1 || month > 12) {	/* check range of month and year */
		FPR(stderr, "%s: month %d not in range 1 .. 12\n", progname,
			month);
		badpar = TRUE;
	}
	
	if (year < MIN_YR || year > MAX_YR) {
		FPR(stderr, "%s year %d not in range %d .. %d\n", progname,
			year, MIN_YR, MAX_YR);
		badpar = TRUE;
	}

	/* command-line errors?  generate usage message and quit */
	   
	if (badpar || badopt) {
		usage(progname);
		exit(EXIT_FAILURE);
		}

	/* flag and numeric parameters OK - now try to open the files */

	if (outfile && freopen(outfile, "w", stdout) == (FILE *) NULL) {
		FPR(stderr, "%s: can't open file %s\n", progname, outfile);
		exit(EXIT_FAILURE);
		}

	/*
	 * In case we don't encounter any year data in the date file,
	 * assume the current year.
	 */
	init_year = year;

	/*
	 * Attempt to open user-specified calendar file first
	 */
	if (date_file != NULL) {
		if ((dfp = fopen(date_file, "r")) == NULL) {
			FPR(stderr, "%s: can't open file %s\n", progname, 
				date_file);
			exit(EXIT_FAILURE);
		}
	}

	/*
	 * Else see if the default calendar file exists (no error if
	 * nonexistent; program will just print empty calendar)
	 */
	else if (nocal == FALSE) {
#ifdef VMS
		strcpy(lbuf, "SYS$LOGIN:");	/* get home directory (VMS) */
#else
		*lbuf = '\0';		/* translate "HOME" to path (Un*x) */
		if ((p = getenv("HOME")) != NULL) {
			strcat(lbuf, p);
			strcat(lbuf, "/");
		}
#endif
		strcat(lbuf, INFILE);
		dfp = fopen(lbuf, "r");
	}

	/*
	 * Write out PostScript prolog
	 */
 	PRT("%%!\n");
 	PRT("/titlefont /%s def\n/dayfont /%s def\n", titlefont, dayfont);

	DOHEADER(header_1);
	if (sun_gray) {
		PRT("\t\t\tday start add 7 mod 1 %s {\n",
			sat_gray ? "le" : "eq" );
  		PRT("\t\t\t\t.8 setgray\n");
  		PRT("\t\t\t} if\n");
		}
	DOHEADER(header_2);

 	PRT("\t%d rotate\n", rotate);
 	if (rotate)
 		PRT("\t50 -120 translate\n");
 	else
 		PRT("\t0.75 0.75 scale\n\t50 460 translate\n");

	DOHEADER(header_3);

	/*
	 * Loop through all the requested months
	 */
	while (nmonths--) {
		pmonth(month, year);
		if (++month > DEC) {
			month = JAN;
			year++;
		}
	}

	if (dfp)		/* close date file */
		fclose(dfp);

#ifdef VMS
	if (default_out)	/* inform VMS users where output is */
		FPR(stderr, "Output is in file %s\n", outfile);
#endif
	exit(EXIT_SUCCESS);
}

/*
 *	Print message explaining correct usage of the command-line
 *	arguments and flags
 */
usage(prog)
	char *prog;
{
	FPR(stderr, "\nUsage:\n\n");
	FPR(stderr, "\t%s [-d FONT] [-e | -f FILE] [-o FILE] [-r] [-s | -%s] [-t FONT]\n",
		prog, S_OPT);
	FPR(stderr, "\t\t[ [ [mm] yy ] | [mm yy n] ]\n\n");
	FPR(stderr, "\t\t-d FONT\t\tspecify alternate font for day names\n");
	FPR(stderr, "\t\t\t\t(default: %s)\n", DAYFONT);
	FPR(stderr, "\n");
	FPR(stderr, "\t\t-e\t\tgenerate empty calendar (ignore date file)\n");
	FPR(stderr, "\n");
	FPR(stderr, "\t\t-f FILE\t\tspecify alternate date file\n");
	FPR(stderr, "\t\t\t\t(default: %s)\n", INFILE);
	FPR(stderr, "\n");
	FPR(stderr, "\t\t-o FILE\t\tspecify alternate output file\n");
	FPR(stderr, "\t\t\t\t(defa
ult: %s)\n", OUTFILE ? OUTFILE : "stdout");
	FPR(stderr, "\n");
	FPR(stderr, "\t\t-r\t\tgenerate portrait-style calendars\n");
	FPR(stderr, "\t\t\t\t(default: landscape)\n");
	FPR(stderr, "\n");
	FPR(stderr, "\t\t-s\t\tprint Saturdays in black\n");
	FPR(stderr, "\t\t-%s\t\tprint Saturdays and Sundays in black\n", S_OPT);
	FPR(stderr, "\t\t\t\t(default: print Saturdays and Sundays in gray)\n");
	FPR(stderr, "\n");
	FPR(stderr, "\t\t-t FONT\t\tspecify alternate font for titles\n");
	FPR(stderr, "\t\t\t\t(default: %s)\n", TITLEFONT);
	FPR(stderr, "\n");
	FPR(stderr, "\t%s [opts]\t\tgenerate calendar for current month/year\n",
		prog);
	FPR(stderr, "\n");
	FPR(stderr, "\t%s [opts] yy\t\tgenerate calendar for entire year yy\n",
		prog);
	FPR(stderr, "\n");
	FPR(stderr, "\t%s [opts] mm yy\tgenerate calendar for month mm\n", prog);
	FPR(stderr, "\t\t\t\t(Jan = 1), year yy (19yy if yy < 100)\n");
	FPR(stderr, "\n");
	FPR(stderr, "\t%s [opts] mm yy n\tas above, for n consecutive months\n",
		prog);
	FPR(stderr, "\n");
}


/*
 * is_valid - return TRUE if m/d/y is a valid date
 */
int is_valid(m, d, y)
	register int m, d, y;
{
	static char len[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

	return (d >= 1 && d <= (len[m] + (m == FEB && is_leap(y))) );
}


/*
 * loadwords - tokenize line buffer into word array, return word count
 * (assumes strtok() has already been called with non-null first arg)
 */
int loadwords()
{
	register char **ap = words;
	register i;

	for (i = 0; *ap = strtok(NULL, WHITESPACE) ; ap++, i++) ;
	return(i);
}


/*
 * parse - check date file entry for (in lbuf[]) for desired month/year
 *
 * Looks for an entry of one of the following forms:
 *
 *	year <year>
 *	<month_name> <day>{*} {<text>}
 *	<month><sep><day>{<sep><year>}{*} {<text>}
 *
 * where
 *	<month_name> := first 3+ characters of name of month (in English)
 *	<sep> := one or more non-numeric, non-space, non-'*' characters
 *
 * Returns day (with holiday flag set if specified) if date file entry
 * is applicable to current month/year; 0 if not; parses any following
 * text into global array words[].
 */
int parse(month, year)
	register int month, year;
{
	register char *cp;
	int mm, dd, yy;
	int is_holiday, valid;

	static char *months[13] = {	/* used to match alpha months */
		"", "jan", "feb", "mar", "apr", "may", "jun",
		    "jul", "aug", "sep", "oct", "nov", "dec",
	};

#define SKIP_FIELD(p) \
	if (1) {while (isdigit(*p)) p++; while (*p && !isdigit(*p)) p++;} else

	/*
	 * Get first field - can be either "year", a month name, or a (complete)
	 * numeric date spec
         */
	cp = strtok(lbuf, WHITESPACE);

	while (*cp) {
		if (isupper(*cp))
			*cp = tolower(*cp);
		cp++;
	}
	cp = lbuf;

	/*
	 * Check for "year" line
	 */
	if (strcmp(cp, "year") == 0) {
		cp = strtok(NULL, WHITESPACE);
		if ((yy = atoi(cp)) > 0) {
			if (yy < 100)
				yy += 1900;
			curr_year = yy;
		}
		return(0);
	}

	/*
	 * If field begins with alpha, try to decode month name
	 */
	if (isalpha(*cp)) {
		/* are month and year the ones we want? */
		if (year != curr_year || strncmp(cp, months[month], 3) != 0)
			return(0);

		/* month found, get and validate day */

		if ((cp = strtok(NULL, WHITESPACE)) == NULL || 
		    !is_valid(month, dd = atoi(cp), year))
			return(0);

		is_holiday = cp[strlen(cp) - 1] == '*';	/* look for holiday flag */

		if (loadwords() || is_holiday)
			return(dd | is_holiday * HOLIDAY);
		return(0);
	}

	/*
	 * Not alpha month, try numeric date (parse completely in case year
	 * has changed)
	 */

	is_holiday = cp[strlen(cp) - 1] == '*';	/* look for holiday flag */
 
	/* get month and compare against desired month */

	valid = (mm = atoi(cp)) == month;
	SKIP_FIELD(cp);

	/* now get and validate day */

	valid &= is_valid(month, dd = atoi(cp), year);
	SKIP_FIELD(cp);

	/* Numeric dates may (or may not) have a year */

	if ((yy = atoi(cp)) > 0) {
		if (yy < 100)
			yy += 1900;
		curr_year = yy;
	}

	valid &= curr_year == year;	/* is year the desired one? */

	/* if date is valid and significant (text or holiday flag), return it */

	if (valid && (loadwords() || is_holiday))
		return(dd | is_holiday * HOLIDAY);
	return(0);
}


/*
 * getday - find next day entry for specified month and year in the date file
 */
int getday(month, year, reset)
	register int month, year;
	int reset;		/* TRUE: rewind date file */
{
	static int eof = FALSE;
	register char *cp;
	register int c;
	int day;
	int in_comment;		/* comments: from '#' to end-of-line */

	if (dfp == NULL)	/* whoops, no date file */
		return(0);

	if (reset) {		/* rewind file, reset default year, clear eof */
		rewind(dfp);
		curr_year = init_year;
		eof = FALSE;
	}

	if (eof)
		return(0);

	do {
		cp = lbuf;
		do {
			in_comment = FALSE;
			while ((c = getc(dfp)) != '\n' && c != EOF) {
				if (c == '#')
					in_comment = TRUE;
				/* ignore comments and leading white space */
				if (in_comment ||
				    (cp == lbuf && (c == ' ' || c == '\t')))
					continue;
				*cp++ = c;
			}
			if (c == EOF) {
				eof = TRUE;
				return(0);
			}
		} while (cp == lbuf);	/* ignore empty lines */

		*cp = '\0';

	/* examine the line, see if its one we want */
	} while ( (day = parse(month, year)) == 0);

	return(day);
}

/*
 * Browse through the date file looking for day text in specified month/year
 */
find_daytext(month, year)
	int month, year;
{
	register char **s;
	register int oldday = -1;
	register int day;

	for (day = getday(month, year, TRUE);
	     day != 0;
	     day = getday(month, year, FALSE))
		if (*words) {
			day &= ~HOLIDAY;
			if (day != oldday) {
				if (oldday != -1)
					PRT("] daytext\n");
				PRT("%d [ \n", day);
				oldday = day;
			} else
				PRT("(.p)\n");
			for (s = words; *s; s++)
				PRT("(%s)\n", *s);
		}

	if (oldday != -1)	/* terminate last call to daytext (if any) */
		PRT("] daytext\n");
}


/*
 * Browse through the date file looking for holidays in specified month/year
 */
find_holidays(month, year)
	int month, year;
{
	register int day;
	unsigned long holidays = 0;

	/* get unique, sorted list of holidays by setting bits in flag word */

	for (day = getday(month, year, TRUE);
	     day != 0;
	     day = getday(month, year, FALSE))
		if (day & HOLIDAY)
			holidays |= 1 << (day & ~HOLIDAY);

	PRT("/holidays [");	/* start definition of list */
	for (day = 1; day <= 31; day++)
		if (holidays & (1 << day))
			PRT(" %d", day);
	PRT(" 99 ] def\n");	/* terminate with dummy entry */
}


/*
 * pmonth - generate calendar for specified month/year
 */
pmonth(month, year)
	int month, year;
{

	PRT("/year %d def\n", year);	/* set up year and month */
	PRT("/month %d def\n", month);
	find_holidays(month, year);	/* first pass - make list of holidays */
	PRT("printmonth\n");
	find_daytext(month, year);	/* second pass - add text to boxes */
	PRT("showpage\n");
}
SHAR_EOF
fi
if test -f 'calendar'
then
	echo shar: will not over-write existing file 'calendar'
else
cat << \SHAR_EOF > 'calendar'
# Sample calendar file for pcal
#
# This should be ~/calendar on Unix, SYS$LOGIN:CALENDAR.DAT on VMS
#
# Valid entries are of the following forms:
#
#	year <year>
#	<month_name> <day>{*} {<text>}
#	<month><sep><day>{<sep><year>}{*} {<text>}
#
# where:
#	<month_name> := first 3+ characters of name of month (in English)
#	<sep> := one or more non-numeric, non-space, non-'*' characters
#	<text> is the text to be printed in the calendar box
#	<day>, <month>, and <year> are appropriate integers
#
#	whitespace is to be used/avoided as implied by the above productions
#	'*' flags the date as a holiday (to be printed in gray)
#	comments run from '#' through end-of-line

year 1990				# set year explicitly

5/28* Memorial Day (observed)		# '*' prints holiday in gray
5/31 Memorial Day

7/4/90* Independence Day		# full date format
7/15 First line of text
7/15 Second line of text

Sep 3* Labor Day			# month written out

10/8* Columbus Day (observed)
10/12 Columbus Day

11/22* Thanksgiving
11/23*					# holiday without text

12/24* Christmas
12/25*

1/1/91* New Year's Day			# set new year implicitly
SHAR_EOF
fi
exit 0
#	End of shell archive