Click here to Skip to main content
15,884,986 members
Articles / General Programming / Algorithms

PsCal - Create Personalized PDF Calendars

Rate me:
Please Sign up or sign in to vote.
4.57/5 (4 votes)
10 Jan 2023CPOL21 min read 5.4K   8   3
This article describes a set of batch, AWK, and PostScript files that together allow you to create personalized, 12-page PDF calendars for some year.

Introduction

This article describes how you can create calendars for various years in the form of multi-page PDF files. An optional, simple text file allows you to personalize any calendars you create.

Here is an example of one month from a calendar created by PsCal:

June 2020

The calendars are created by a set of files that I refer to as PsCal. PsCal generates its calendars using two ancient programming languages: AWK and PostScript.

About AWK

AWK was developed in the 1970s at Bell Labs and is still in wide use today. It is a tool that was designed to allow you to quickly process text files to extract information contained therein. The GNU version of AWK, GAWK, is well-maintained by Arnold Robbins (and has been for many years). GAWK is the version of AWK used by PsCal.

In the PsCal zip file, I provide a file, _awkinfo.txt, for anyone who is unfamiliar with AWK. It contains quite a bit of information about AWK that should help you understand the AWK code that is part of PsCal. My hope is that even seasoned users of AWK may find some new and somewhat useful information there.

About PostScript

PostScript was introduced in the mid 1980s by Adobe Systems. It provides a method of describing a printed page in a way that does not depend on the device used to display the page (other than the fact that it can decipher the PostScript language).

In the PsCal zip file, I provide another file, _psinfo.txt, for anyone who is unfamiliar with but curious about the PostScript language.

Are AWK and PostScript Relevant?

While the AWK language is very much alive and well, the same is not true of PostScript. That is, Adobe no longer develops PostScript; it has replaced PostScript with the PDF format. A cursory look at the PDF version 1.7 spec, however, shows that the PDF format was strongly influenced by PostScript. In that light, a little PostScript knowledge can be helpful as a precursor to learning about PDF.

Even though PostScript was developed to describe a printed page, it is a general-purpose language. You can use it to do all kinds of projects. Some of the more fun ones involve making drawings reminiscent of the ones you can make using the Spirograph toy. Here is part of an example PDF created from the SpiralSquares.ps program that you can find in the postscript\examples directory of the PsCal zip file:

Square Spirals

Although it does not appear to be so at first glance, the example above is composed entirely of squares!

The Beginnings of PsCal

Way back in 1993 when I was working at Texas Instruments, I found a UNIX script that created a nice looking PostScript calendar for a single month. That script would place events on the calendar that you supplied in a text file. To the best of my recollection, that script placed few, if any, of the normal holidays on the calendar, unless you included them in the text file.

Over the years, I moved away from UNIX and into the Windows world, and in the process, I rewrote PsCal as a Windows command shell application. I also made quite a few changes and enhancements to the original implementation along the way.

Before getting into the details of how PsCal creates a calendar, I want to devote a bit of text to the part of PsCal that exists to make calendars relevant to you--the user events file.

The User Events File

Shortly, you will learn how you can easily create an example user events file. The intent of this file is to demonstrate some of the things you can do with PsCal, and to give you a well-commented file that you can edit to make calendars that pertain to you.

There are two general types of things that can appear in a user events file: events to be placed on the calendar and things that affect the calendar's overall appearance.

User Event Definitions

A user event is something pertinent to you that you would like to place on a calendar. Things you would likely want on your calendars are birthdays and anniversaries. You could also add things like appointments, and, say, a concert or date that occurs only one time.

The format of a user event is:

<month>:<day>[;<option1>[;<option2>[;...]]]:<text>

The month can be 1-12 or *, while the day is either 1-31 or a <daycode>. The text to be printed is the last item on the line. Options can appear between the day/daycode and the text, separated from the day/daycode and from each other by a semi-colon.

One nice feature of PsCal is useful for any type of event that repeats every year like a birthday. If the text you want printed for an event contains a 4-digit year surrounded by exclamation marks, then when the event is placed on the calendar, that text will be replaced by something like "25th". For example, if the text for some event was "Dad's !1962! BDay", and that event was added to a calendar for 2023, the text placed on the calendar would be "Dad's 61st BDay". The same event appearing on a calendar for the year 2000 would read "Dad's 38th BDay"

As you can see, this feature will keep you up-to-date with people's ages, as well as which anniversary of some special event is approaching.

The format of events is commented in detail in the example events file that you can create, so I won't burden this article with many more details. However, a short discussion is warranted of the options that are available. Also, in a following section on Roaming holidays, I will shed some light on the <daycode> construct that can appear in place of the <day>.

Event Options

There are several options that can modify the default appearance of a calendar day. Two of these, font and fmt can change the font used to print the event's text and the justification of the text (left, right, centered or full).

Other options include: year for events that happen in a particular year, repeat for events that repeat every so many days, and gfn which is short for graphics function. There are several of these graphics functions that can be used alone or in conjunction with one another to "dress up" an event.

Modifying the Calendar Appearance

In addition to events, the user event file can contain items that affect the calendar's general appearance. These items all begin with "@", and they include things like fonts to be used, default event text formatting, header and footer text, and PostScript code that could be graphics functions and/or images.

As with events, these types of items are commented in detail in the example events file, so no more effort is devoted to them here. Instead, let's see what steps are taken by PsCal to create personalized calendars.

How PsCal Creates Calendars

The calendar generation process employed by PsCal consists of three major parts. It begins with a Windows batch file that accepts several optional parameters. These optional parameters can modify the calendar to be created in various ways.

The batch file passes its parameters to GAWK, having it carry out the program contained in the pscal.awk file. This AWK program creates a PostScript version of the requested calendar and returns a value to the batch file indicating success or failure.

In the successful case, the batch file then executes the ps2pdf batch file from the GhostScript installation. That batch file converts the PostScript file created by GAWK into a PDF file.

MakeCalendar.bat

To create a calendar for the current year, all you need do is execute the MakeCalendar batch file with no parameters. By default, it will create a 12-month calendar for the current year in a file named <current year>.pdf.

Also, by default, if there is a file named events.pse in the current directory, the events in that file will be placed on the calendar. This file is used whenever it is present in the current directory, unless you supply the name of a different file on the command line. Usage for MakeCalendar.bat is:

MakeCalendar [OPTIONS]

OPTIONS can be one or more of:

  -y=<Year>       The year for which the calendar is to be generated. DEFAULT:
                  the current year.

  -m=<MonthList>  A list of months to be included in the calendar.  DEFAULT:
                  1-12.  Months are numbered from 1 (Jan) through 12 (Dec).
                  Specify a range of months by placing a dash (-) between two
                  months. Use a comma (,) to separate individual months and
                  ranges of months.

  -e=<EventsFile> The name of a file containing events specific to you, such as
                  birthdays, anniversaries, paydays, etc. DEFAULT: a file named
                  "events.pse" in the current directory, if it exists.  This
                  file can also contain data that can modify the appearance of
                  the calendar, such as headers and footers, fonts, and various
                  graphics effects.

  -n=<BaseName>   The base name of two output files that are created. One is
                  a PostScript file (which is deleted by default), and one is a
                  PDF file. Each of them will contain the calendar for the year
                  in (obviously) different formats.  DEFAULT: the current (or
                  given) year, producing <year>.ps and <year>.pdf.

  -example        Create an example events file you can edit to add events like
                  birthdays, anniversaries, appointments, paydays, etc. to the
                  calendars you create.

As far as Windows batch processing goes, MakeCalendar is fairly straightforward. One of the more useful batch features that it uses is to set an environment variable to the value of some program's output. For example, this line:

for /f %%y in ('gawk "BEGIN{print strftime(\"^%%Y\")}"') do set BASENAME=%%y

sets the variable BASENAME to the current year which is output by GAWK executing the strftime() function.

pscal.awk

The bulk of the work performed by the AWK portion of PsCal is to create a PostScript array that defines the events that will appear on the calendar. Those events are several U.S. and worldwide holidays, as well as the events that are defined in any events file that is to be used. For all practical purposes, event and holiday are interchangeable terms when used in conjunction with PsCal.

Parsing the User Events File

When a user events file has been supplied to be used, it is parsed at the top level by code at the beginning of function GatherEventsAndData():

# Start with any user-supplied events, code, and/or configuration settings.
if (length(UserEventsFile)) {
    while (ReadLine(UserEventsFile) > 0) {
        if ($1 ~ /^@/) {        # Handle lines that begin with "@".
            HandleCommandOrCode(UserEventsFile,FontArray,EventArray)
        }
        else {                  # Handle lines that should contain an event.
            HandleUserEvent(UserEventsFile,EventArray)
        }
    }
    close(UserEventsFile)
}

The ReadLine() function reads lines from the user events file until it finds one that is non-blank and not a comment (i.e., its first non-blank character is not "#") or it reaches the end of the file (returning 0 to the caller).

When ReadLine() returns a line, that line might begin with "@", in which case it should contain information that can or will affect the appearance of the calendar in some way. If it begins with some other character, it should contain an event description.

The if test, $1 ~ /^@/, is straightforward AWK code. It tests whether the first string of non-blank characters on the line ($1) matches (~) a regular expression (/^@/). That regular expression says the text begins (^) with the @ character.

So if the first non-blank character of the returned line is "@", that line is handled by the function HandleCommandOrCode(). That function processes things in the file such as font definitions (saved in FontArray[]), calendar headers and footers, and PostScript code that might include graphics functions and user-defined images. It also handles any groups of repeating events that may be defined, calling function HandleUserEvent() to do the work for each of the events in the group.

The non-@ lines of the user events file are passed to function HandleUserEvent(). This code checks to see that the line does indeed describe an event. If it does, and if the event should be included on the calendar, it is added to EventArray[]. The only reasons for an event to be excluded are:

  1. the event happens only in a specific year which is not the year of the calendar being created
  2. the event contains no information that would have a visible effect, or
  3. the line is not recognized as a valid event.

Once the user events file has been processed (if one was provided), the AWK script adds various holidays, and sometimes moon phases, to EventArray[].

Roaming Holidays and Events

Many holidays like New Year's Day and Christmas occur on the same date every year, so describing when they should appear on a calendar is straightforward. However, there are other holidays like Thanksgiving and events like election day that occur on different dates from year to year. Also in this category are events that happen, say, on the last weekday of the month.

The daycode construct is capable of handling many of these events. It has the form <occurrence>,<day of week> with both of these items being single digit integers.

<occurrence> is in the range from 1 (first) to 5 (fifth) or it is 9 (last); <day of the week> is in the range from 0 (Sunday) to 6 (Saturday) or it is 9 (weekday). Together, these two number ranges allow you specify, for example, dates like the 2nd Sunday (2,0), the last Thursday (9,5), or the last weekday (9,9) in a month.

Adding Missing Holidays of Importance to You

Several of the holidays included in PsCal are associated with the U.S. However, it is very easy to add others to the pscal.awk program.

Function AddStandardHolidays() is where you would add missing holidays of importance or delete ones that have no particular significance to you. Here are two of the many lines from that function that define two U.S. holidays:

EventArray[++gEventCount] = SepSprint("07","04",";gfn=Gbottom_text;","!1776! 
                            Independence Day")
EventArray[++gEventCount] = SepSprint("02", ConvertDayCode(gYear,2,"3,1"), 
                            ";gfn=Gbottom_text;","Presidents' Day")

These lines assign the return value of the SepSprint function to entries in EventArray[]. SepSprint simply returns a string containing its input strings separated by another string that is stored in the global variable gSep: "```". So, the first line above is equivalent to:

EventArray[++gEventCount] = "07```04```;gfn=Gbottom_text;```!1776! Independence Day" 

This is just a handy way to store multiple strings in a single array entry in a way that they easily can be recovered using various features of AWK. You can see how the recovery is done if you look at the code at the beginning of function WriteEventData().

The strings passed to SepSprint in this case are, in order: the month, day of the month, a reference to the graphics function used to place the text at the bottom of the day's calendar square, and the text that identifies the holiday.

Note that the second assignment does not (directly) specify a day of the month when Presidents' Day occurs. Since this day occurs on the 3rd Monday of February, the ConvertDayCode() function is used to convert that code into the day of the month in the year of the calendar (gYear).

Hopefully, it is clear from this discussion that if you wanted to add England's early May bank holiday to your calendars, you could do so by adding a line like the following to function AddStandardHolidays():

EventArray[++gEventCount] = SepSprint("05", ConvertDayCode(gYear,5,"1,1"), 
                            ";gfn=Gbottom_text;","Bank holiday")

Arranging Events

In PostScript, anything drawn on a page totally obscures anything that might have been drawn there previously. Because of this, if some calendar day happens to have multiple graphics functions defined for it, those functions need to be applied in an order that provides the best overall appearance.

Once all the events for a year have been added to EventArray[], the function ArrangeEvents() will make any necessary adjustments by calling two other functions: FindDaysWithMultipleEvents() and RearrangeMultiEventDays():

# Search EventArray[] to create an array, M[], of size MultiEventDayCount.
# Its entries will be the starting and ending indices of 2 or more events
# in EventArray[] that occur on some single day.
MultiEventDayCount = FindDaysWithMultipleEvents(EventArray,M)

# Now rearrange any days in EventArray[] that have multiple events.
if (MultiEventDayCount > 0) {
    RearrangeMultiEventDays(EventArray,M,MultiEventDayCount)
} 

FindDaysWithMultipleEvents() sorts the entries in EventArray[] by month and day. Then it walks the array looking for multiple entries on the same month and day.

If it finds some, it creates another array, M[], that holds pairs of starting and ending indexes within EventArray[] of events occurring on a single day. For example, if the 5th through 8th entries of EventArray[] happen on the same day, then some entry would exist in M[] such that M[i] = "5 8". The return value of FindDaysWithMultipleEvents() is the number of entries in M[], which is the number of days on which two or more events occur.

If there are any entries in M[], then RearrangeMultiEventDays() is called to ensure the multiple events on each of the days are ordered in the best way possible. Performing this feat, however, is somewhat complicated. For each day having multiple events:

  • Make a copy of the events for that day in another array.
  • Sort that array by the line number within the user events file where the event was defined (so that multiple events with text to be printed are printed in the order the user placed them in the file).
  • Repeatedly look through the events in the copy for events using individual graphics functions in a priority order. When the first is found, copy the event over the first entry in EventArray[] for the day. Continue doing so until all graphics functions in use have been located and copied to the next location in EventArray[]. If any events remain in the copy, simply copy them in order into the remaining entries in EventArray[].
  • If there is a user-defined graphic (i.e., "gfn=StartImage ...") on some day, make sure that no bitmap or shading is drawn over it by simply deleting the bitmap or shading event if present.
  • Finally, if there are multiple events using the Gbottom_text function, consolidate them into one event with the various texts separated by ";".

The effect of these two functions and the ones they call is to, for example, rearrange these June 16th event definitions in the user events file from this:

...
6:16;gfn=Gburst_day;fmt=c;font=BigBoldItalic:!1984! \n Annual \n Special \n Day
6:16;gfn=2 Gbox_day
6:16;gfn=3 G3D_day
... 

into these event entries in the PostScript event array:

...
[  6 16 fmt_c  0 {3 G3D_day} () ]
[  6 16 fmt_c  4 {Gburst_day} () ]
[  6 16 fmt_c  0 {2 Gbox_day} () ]
[  6 16 fmt_c  4 {} (36th \n Annual \n Special \n Day) ]
... 

The G3D_day graphics function is used to make a calendar day stand out (as if it is 3-D). When it is used, it needs to be applied before any other function. So, even though it is the last function listed on June 16th in the user events file, it becomes the first event in the PostScript event array for that day. The Gburst_day function has a higher priority than Gbox_day, so they remain in their original order.

You may have noticed that the text that was specified along with the Gburst_day function has been separated out into a "text-only" event, making four PostScript event array entries. This is done to ensure that the text for an event is printed last after any graphics functions that may appear on some calendar day.

PsCal PostScript Code

If you would like to view the PostScript code for a calendar, you can modify a line in the MakeCalendar batch file, changing the setting of variable DEL_PS_FILE from 1 to 0. After you do so, the batch file will no longer delete the PostScript version of any calendar it creates.

Some of the PostScript code that is used to make a calendar is created by the AWK script: boilerplate code at the start of the file, code related to page generation at the end of the file, the event array, and any font data or PostScript code that was contained in the user events file.

The bulk of the PostScript code, however, is not calendar specific and is simply copied from two files in the postscript directory: PsStart.txt and PsEnd.txt. This code was originally written by Patrick Wood of Pipeline Associates, Inc., an entity that seems to no longer exist:

Copyright 1987 by Pipeline Associates, Inc.
Permission is granted to modify and distribute this free of charge, as
long as the above copyright notices and this statement are included. 

Over the years, I have made several changes and additions to this code. For example, I added any and all PostScript code related to:

  • bitmaps and images
  • graphics functions for "dressing up" chosen days
  • moon phases
  • embedding newlines in text to be printed
  • showing the day of the year along with the date
  • generating a calendar for an entire year (12 pages) instead of a single month (1 page)

Unfortunately, comments were somewhat lacking in the original PostScript code. I have added more comments here and there where I needed to decipher something in order to make an addition or modification, but I have yet to bring the code up to a higher standard, whether it be in the formatting or commenting of the code. My apologies.

Also, if you decide to dive into the PostScript code for a calendar, be aware that the code uses numerous global variables and has no discernible naming convention to help you recognize them. And finally, because of the stack-based nature of PostScript, there are places where you'll need a good memory of what's on the operand stack.

Main PostScript Code

PostScript does not permit execution of functions that have not yet been defined. Therefore, you need to scroll to the end of a PostScript file to see what it is trying to accomplish.

In the case of PsCal, you will find some code created by the CreateMonthPages() function in pscal.awk. Here is a sample of that code:

%%Page: January 1
1 DoOneMonth
showpage

%%Page: February 2
2 DoOneMonth
showpage

%%Page: March 3
3 DoOneMonth
showpage

/month 4 def
/DayOfYear DayOfYear ndays add def

/month 5 def
/DayOfYear DayOfYear ndays add def

%%Page: June 4
6 DoOneMonth
showpage

... 

The %% digraph begins a Document Structuring Comment (DSC). In the case of %%Page: the month name is the name of the page, and the integer after that is the number of the page among all the pages in the file. In the above example, June is the 4th page because April and May were not included in this particular calendar.

Since April and May are excluded from this particular calendar, all that need happen is for the number of days in those months to be added to the global variable DayOfYear, which is used to print the day of the year near the day of the month.

What is not obvious about the April and May code is that ndays is a function that calculates the number of days in a month. Also not obvious is that the function needs the variable month to be defined in order to return the correct number.

Two other functions are called in this section of the file for each month that is included: DoOneMonth and showpage. DoOneMonth requires an integer on the stack when it is invoked. This integer indicates the month of the year for which it should create a page. Whenever a month is included, the PostScript showpage procedure is called to form the page on the output device.

Function DoOneMonth

This function creates the image that represents the month. That would include the events and holidays for the month contained in the /holidays array (created by the pscal.awk program from the contents of EventArray[]).

DoOneMonth calls on another major function, calendar, to draw not only the big, full-page calendar for the month, but also the two mini-calendars for the prior and following months. DoOneMonth sets a variable, IsSubMonth, to control what calendar does, as well as using the PostScript scale and translate operators to good advantage to place and shrink the mini-calendars and their text.

Function drawevents

This function is called when the calendar function is drawing the main calendar for some month; it walks the /holidays array looking for events that occur in the current month. As it finds them, it calls function prtevent to handle the printing of individual event text. The PostScript code at this point becomes fairly involved with text formatting involving the various types of text justification and the breaking of longer event text into multiple lines. If you are sadistic enough, you just might enjoy a stroll through that code!

PostScript to PDF Conversion

For many years, I used GSview to view the calendars I created. Some time ago, I found that GhostScript could convert PostScript files into PDF files. Since PDF is a widely supported format, I put some effort into integrating GhostScript into PsCal.

Unfortunately, the GhostScript installation is quite large, so I only provide information about how to download and add it to a PsCal installation. You can read about how to do that in the _readme.txt file that is in the PsCal zip file.

Fun Stuff Along the Way

I've had some fun and some frustration over the years as I worked on PsCal. I'll briefly mention a few here.

Number Endings

One of the fun things I had to figure out was how to return the proper ending for a number that is the Nth in a series. That is, what is the proper 2-character string to append to various numbers? For example, 50th, 1st, 133rd, and so on.

This task is carried out by one of my library functions, LibGetNumberEnding(), which is located in the Math.awk library. It seems like it would be trivial to write, but it did take a bit of thought for me to get right (hint: the teens need to be dealt with).

You might enjoy trying to write some code in your language of choice that carries out this task.

What Month Contains the Nth Day of the Year?

The answer to this question is provided where needed by PsCal by another of my library functions, LibMonthContainingDayOfYear() that is in the TimeAndDate.awk library.

There is nothing particularly tricky about the way this function works, but it would be interesting to see if you can come up with a way to do this that is based on some different approach.

Easter

There were things I needed to know to get PsCal where I wanted that I could not come up with on my own. If you've ever delved into the Easter holiday, specifically when it occurs, then you may know where this is headed.

The function AddEasterAndRelated() in the pscal.awk file handles adding Easter and its related holidays to the event array. The comments I wrote for this function include: "The most bizarre of holidays to compute, Easter, falls on the first Sunday after the first ecclesiastical full moon that is on or after March 21, aka the Paschal Full Moon (PFM)."

That pretty well tells me I'm not going to be able to figure out when Easter occurs on any given year. Fortunately, I was able to find an algorithm that can do so here: http://almanac.oremus.org/easter/computus/

Conclusion

Although AWK and PostScript have been around for decades, they can still be used for both work and fun. I hope that PsCal is a decent example of their usefulness, and that the PostScript programs in PsCal's postscript\examples directory show some of the fun you can have.

I believe that most programmers could benefit from a working knowledge of AWK. I know that using anything other than the language of the day is anathema to many folks these days, but some tasks can be done so simply with AWK that it deserves at least a tiny bit of consideration.

How many languages exist that are so concise they can print the contents of one or more files along with line numbers using a smaller program than this:

{print FNR,$0}

I admit that I do not know the answer to that question, but I suspect that if it is non-zero, it is fairly small.

History

  • 6th January, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Retired
United States United States
In my teens I enlisted in the U.S. Air Force as an imagery interpreter. While in the USAF, I graduated from The University of Texas at Austin with a BS degree in Electrical Engineering concentrating in computer engineering. I then got a commission and finished out my USAF career as a computer wargaming programmer.

After about 13 years, I left the Air Force for Texas Instruments, first working on a military project, and later working with ASICs, PC chipsets, and notebook BIOS.

When TI sold their notebook division to Acer, I moved on to Dell working as a notebook BIOS engineer, Windows programmer, and server BIOS engineer.

I love hiking in Utah, especially The Needles area of Canyonlands NP. I enjoy instrumental post-rock music, disc golf, and books about J. Robert Oppenheimer and the Manhattan project.

My favorite math fact: The limit as N approaches infinity of (1 + x/N)^N = e^x

I am retired now and loving it!

Comments and Discussions

 
QuestionDiacritical letters Pin
frogcoder510-Oct-23 17:06
frogcoder510-Oct-23 17:06 
AnswerRe: Diacritical letters Pin
FormerBIOSGuy18-Oct-23 5:50
FormerBIOSGuy18-Oct-23 5:50 
AnswerRe: Diacritical letters Pin
FormerBIOSGuy21-Mar-24 12:27
FormerBIOSGuy21-Mar-24 12:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.