This directory contains some PostScript programs that may generate nice looking (at least to my eyes) printed calendars when sent to a PostScript printer. Not elaborate calendars with local and/or variable holidays like Easter (you may want to check emacs for this) but still very usable.
Two group of calendar utilities are considered: the first one will print a single month calendar on a paper sheet (month-family); the second one (year-family) will print a whole year calendar on a paper sheet. At the end, we will make some considerations about calculations connected to calendars, with the corresponding C++ code.
Name | Purpose |
---|---|
month.ps | Calendar for a full month; English; weeks start on Sundays; no week numbers. |
month_iso.ps | Calendar for a full month; English; weeks start on Mondays; week numbers. |
month_it.ps | Calendar for a full month; Italian. |
month | Shell command procedure |
year_eng.ps | Calendar for a full year; English. |
year.ps | Calendar for a full year; Italian. |
All these programs are based on the same original code (public domain), that I retrieved from (I think) comp.sources.postscript; it was labeled:
% PostScript program to draw calendar % Copyright (C) 1987 by Pipeline Associates, Inc. % Permission is granted to modify and distribute this free of charge.
That original code had some problems:
I have also prepared an Italian version: apart from the trivial differences like the month names, the notable change is that the weeks start on Monday in the Italian version; while the original one had the weeks starting with Sunday. The International Standardization Organization (ISO) in its document ISO-8601 (see again, for more informations, the section "The ISO week numbers") states that the weeks must start on Mondays; but in the United States of America (and, to a lesser extent, in other countries of the English-speaking world), it is normal to regard weeks starting with Sundays. So, I have here two English versions: month.ps and month_iso.ps, the first one still using the original convention and the second one assuming ISO-8601; and an Italian (also IS0-8601 compliant) version month_it.ps. In addition, in the Italian version Saturdays are printed as normal weekdays (i.e. in black) and not as holidays (in gray).
The printout of the ISO week numbers in the calendar makes sense only if all the days in the same row belong to the same ISO week; that in turn implies that that printout has no sense if the week starts with Sundays (and is not inserted, then, in month.ps).
To use these programs, save the source files and edit them: search, near the end, the two lines saying something like:
/month 12 def /year 2001 def
and change the above values to the month number (1...12) and the year number (after 1800) you need; and send the file to a PostScript printer (or use ghostscript, or whatever PostScript viewer you have available). BE CAREFUL: no tests are made for valid values, e.g. for a month value less than 1 or bigger than 12.
If you want to customize the calendar for your language, change appropriately the month and weekday names; if your localized names have accents, you have to re-encode the used fonts: and this is a procedure described e.g. in the PostScript Cookbook (the so-called Blue Book). Don't ask me, please.
Another interactive version is a shell script (month), that gets from the command line parameters the values of both month and year and then copies a PostScript calendar file of the month family to the standard output making the substitution. In the last line of the shell script there is the name of the file to be copied; you may want to change it.
The year-oriented calendars are similar, and completely written by me; to use them, find (near the end) the line saying:
/year 2001 def
(or something similar) and change the number according to your needs. Their names are year.ps (in Italian) and year_eng.ps (in English).
The length of a tropical year, i.e. the time between two consecutive vernal equinoxes, varies; its mean value is currently about 365.242 days. In order to compensate the difference between the calendar and the solar year, leap years are used. In the Julian calendar (introduced in the year 45 B.C. by Julius Caesar) every fourth year was a leap year, giving to it an average length of 365.25 days.
In the sixteenth century, the date of the spring equinox had shifted, as a consequence of the difference between the lengths of the calendar and of the solar year, from March 21 to March 11; the Pope Gregory XIII stated that century years not divisible by 400 were not leap years (giving to the calendar year a mean length of 365.2425 days), and corrected the accumulated 10 days error by proclaiming that Thursday, October 4, 1582 (the last date of the Julian calendar) would be followed by Friday, October 15, 1582 (the first day of the so-called Gregorian calendar). The calendar itself was proposed by the physician Aloysius Lilius, approved by the Council of Trent (1545-1563) and decreed in the 1582 bull "Inter Gravissimas".
A C++ snippet that tests for leap years is the following:
inline bool isLeap(int year) { return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)); } |
(the code is in this file). Italy, Spain and Portugal adopted immediately the new calendar, as did the catholic states in Germany; the protestant part of Germany waited until 1700. Great Britain and its colonies (United States included) waited until 1752, Russia held out until after the revolution in 1918; and the last countries that joined the club were Greece (1924), Turkey (1927) and China (1949).
As a side note, the astronomer John Herschel proposed a better approximation (1 year = 365.24225 days) suggesting that, in addition to the above rules, every year divisible by 4000 should not be a leap year; but this rule has never been adopted.
The Zeller's congruence method is the name of a well-known algorithm that computes the day of the week of a given date, from the numeric values of day, month and year. In 1882 Christian Zeller, a clergyman, published on Württembergische Vierteljahrshefte für Landesgeschichte, Jahrgang V, pages 313-314, a first article titled Die Grundaufgaben der Kalenderrechnung auf neue und vereinfachte Weise gelöst (or The Basic Problems of the Calendar-Reckoning Solved in a New and Simplified Manner); and, one year later, in Bulletin de la Société Mathématique de France, vol. 11, pages 59-61, another article titled Problema Duplex Calendarii Fundamentale — i.e. About Two Fundamental Problems of the Calendar. These two problems were the above one, and the computation of the Easter for every year. Later, in Acta Mathematica vol. 9 (1886-87), pages 131-136, his final work on both these subjects appeared, under the title Kalender-Formeln.
The hearth of the Zeller's congruence method (in short) is the observation that, starting from March considered the first month and ending with January (February is of course special), the sums of the last decimal digit of the month-lengths may be computed using the empirical formula ((3*M+2) div 5), where M is the month (March=1, January=11) and div denotes an integer division without remainder.
Starting from that remark, several formulas have been written to compute the day of the week; the one used in the calendar PostScript program is equivalent to the following computer program snippet in C++:
int weekDay( int day, int month, int year ){ // Returns the day of the week for the given date, assumed valid and // in the Gregorian calendar. The returned value is: 0 for Sunday, // 1 for Monday, ..., 6 for Saturday; or, if the preprocessor symbol // MONDAY_IS_0 is defined: 0 for Monday, 1 for Tuesday, ..., 6 for // Sunday. int result; if (month < 3) { month += 12; --year; } result = day + (13 * month - 27)/5 + year + year/4 - year/100 + year/400; #ifdef MONDAY_IS_0 result += 6; #endif // MONDAY_IS_0 return (result % 7); } |
(the code is in this file). That variant has been chosen because, before the modulus operator, "result" is guaranteed to be positive. In the ISO-compliant versions, i.e. these having the weeks starting on Mondays, it is better to have a returned value defined as 0 for Monday, 1 for Tuesday and so on, ending with 6 for Sunday: in order to obtain that, at the value of "result" is still added 6, before the modulus operator in the return statement.
Astronomers use a particular reckoning of the time, that is very useful for computer scientists: they have chosen an arbitrary starting point quite far from us in the past; and specify dates as Julian Day Numbers, counting days backward and forward starting from that arbitrary origin. That method has been introduced in 1583 by Joseph Justus Scaliger; the name Julian comes from Scaliger's father (Julius Caesar Scaliger), and Julian day zero corresponds to a virtual Monday, November 24, 4713 B.C. of the Gregorian Calendar: virtual in the sense that the Gregorian Calendar has been introduced in 1582, and the "year 4713 Before Christ" did not exist at all in the real world. To be precise, the origin of the Julian time coordinates is at 12 noon UTC of the above date: e.g., Julian Day 2440000 began at noon UTC of May 23, 1968; and at noon UTC of January 1, 2000 began Julian Day 2451545.
If the Julian day number corresponding to a given date is known, its day of the week may be obtained with a straightforward calculation: since Julian day 0 was a Monday, the remainder of the division of the Julian day number by 7 is 0 if that day was a Monday, 1 if it was a Tuesday, and so on.
The following snippet, adapted from Press, Teukolsky, Vetterling and Flannery's "Numerical Recipes in C (2nd Ed.)", published by Cambridge University Press in 1992, shows how to convert a Gregorian date to a Julian Day number:
#include <stdexcept> // Gregorian Calendar started on October 15, 1582 const long gregorianStart = (31L * (10 + 12L * 1582) + 15); long greg2jul( int day, int month, int year ) { // From "Numerical Recipes in C", adapted if (year == 0) throw std::domain_error("There is no year 0"); // There is no calendar year 0. As a consequence, e.g., the year 10 // BC of the calendar is the year -9 of the Julian system. The // formal parameter "year" of this procedure is the *calendar* year, // positive (AD) or negative (BC). long jy = (year < 0 ? year + 1 : year); long jm; // Months before March are considered the last months of the // previous year if (month > 2) jm = month + 1; else { --jy; jm = month + 13; } long result = day + 1720995L; result += static_cast<long>(365.25 * jy); result += static_cast<long>(30.6001 * jm); if ((31L * (month + 12L * year) + day) >= gregorianStart) { long cent = static_cast<long>(0.01 * jy); result += 2 - cent; result += static_cast<long>(0.25 * cent); } return result; } |
(the code is in this file). The following snippet, based on the Common Lisp code found in "Software, Practice and Experience", Vol. 23 (1993) in page 384, shows how to perform the reverse conversion from a Julian Day number to a Gregorian date:
#include <iostream> #include <sstream> #include <stdexcept> // Gregorian Calendar started on Julian Day 2299161 const long gregorianEpoch = 2299161L; const int nMonths = 12; static int monthDays[nMonths + 1] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; void jul2greg( long jul, int & day, int & month, int & year ) { // From "Software, Practice and Experience", Vol. 23 (1993), // pag. 384 if (jul < gregorianEpoch) { std::ostringstream s; s << "Julian Day " << jul << " comes before Gregorian Calendar"; throw std::domain_error(s.str()); } // JD 1721425 corresponds to a (virtual) January 1, year 1 of the // Gregorian Calendar. n400 is the number of completed 400 year // cycles; n100 the number of completed 100 year cycles not included // in n400; n4 the number of completed 4 year cycles not included in // n400 and n100; n1 the number of years not included in n400, n100 // or n4. jul -= 1721426; long n400 = jul / 146097; jul %= 146097; long n100 = jul / 36524; jul %= 36524; long n4 = jul / 1461; jul %= 1461; long n1 = jul / 365; jul %= 365; year = 400 * n400 + 100 * n100 + 4 * n4 + n1; if (n100 == 4 || n1 == 4) { // Day 366 of year "year" day = 31; month = 12; } else { // Day "jul+1" of year "year+1" if (isLeap(++year)) monthDays[2] = 29; else monthDays[2] = 28; ++jul; for (int i = 1; i <= nMonths; ++i) { if (jul <= monthDays[i]) { day = jul; month = i; return; } jul -= monthDays[i]; } std::cerr << "Can't happen\n"; } } |
(the code is in this file).
The International Organization for Standardization has provided definitions both of a week and of a Week-of-the-Year Number in the document ISO-8601; as already noted, the definition of a week is not widely accepted in the English-speaking world.
In addition, there are many other possible definitions of the Week 1 of a year (from which the other weeks follow naturally); companies often choose their own standards, it seems, and they are sometimes quite different. In the USA, Week 1 is in general the week, starting on Sunday and ending on Saturday, which contains January 1st; and may be numbered as "week 1" or "week 0". Others choose the first full week of the Calendar year, which is the one containing the 7th of January.
ISO-8601 defines the week as starting with Monday and ending with Sunday; and the first week of a year as the first week which is mostly within the Gregorian year. That last definition is equivalent to the following two variants: the week which contains the first Thursday of the Gregorian year; and the week containing January 4th.
The ISO-8601 week numbers are integers from 1 to 52 or 53; parts of week 1 may be in the previous calendar year; parts of week 52 may be in the following calendar year; and if a year has a week 53, parts of it must be in the following calendar year. For that reason, the correct way to quote an ISO week number is YYYY-WW.
As an example, in the year 1998 the ISO week 1 began on Monday December 29th, 1997; and the last ISO week (week 53) ended on Sunday January 3rd, 1999. So December 29th, 1997, is in the ISO week 1998-01; and January 1st, 1999, is in the ISO week 1998-53.
The following C++ snippet evaluates the ISO week of the year for every valid date of the Gregorian calendar; it relies on another procedure weekDay that computes the day of the week in the ISO way, i.e. returning 0 for Monday, 1 for Tuesday and so on, and finally 6 for Sunday.
#include <iostream> long week( int day, int month, int year ){ // Returns the number of the ISO week in the year (from 1 to 53) // containing the given date (assumed valid, and in the Gregorian // calendar); more exactly, the returned value is yyyyww, where yyyy // is the year and ww the ISO week number. int m, start, yearDay, lastDay, lastSunday; const int nMonths = 12; static int monthDays[nMonths + 1] = { 0, 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; // - Initializes correctly the number of days in February; // - stores in lastDay the total number of days in the given year; // - stores in yearDay the total number of days in the year not // after the given date ("day-of-the-year"). if (isLeap(year)) { monthDays[2] = 29; lastDay = 366; } else { monthDays[2] = 28; lastDay = 365; } for (m = 0, yearDay = day; ++m < month; ) { yearDay += monthDays[m]; } // - Computes in "start" the first day of the first week of this // year ("start" may be negative if the beginning of the first // week is in the previous year); // - computes in "lastSunday" the date of the last Sunday in // December. // // weekDay(day, month, year) is a procedure that computes the day of // the week corresponding to a given date, returning 0 for Monday, 1 // for Tuesday, ..., 6 for Sunday. start = 4 - weekDay(4, 1, year); lastSunday = weekDay(31, 12, year); lastSunday = lastSunday == 6 ? 31 : 30-lastSunday; std::cout << "- The first ISO week of the year " << year << " begins on Monday "; if (start < 1) { std::cout << 31+start << "-Dec-" << year-1 << std::endl; } else { std::cout << start << "-Jan-" << year << std::endl; } // After the following printout, "lastSunday" is changed to the // day-of-the-year number of the Sunday ending the last ISO week of // the current year. std::cout << "- The last ISO week of the year " << year << " ends on Sunday "; if (31-lastSunday < 4) { std::cout << lastSunday << "-Dec-" << year << std::endl; lastSunday = lastDay - (31 - lastSunday); } else { std::cout << lastSunday+7-31 << "-Jan-" << year+1 << std::endl; lastSunday = lastDay + lastSunday+7-31; } // - If the day-of-the-year is before "start", we are in the last // week of the previous year (the week of December 31); // - if not, and if the given date is after "lastSunday", we are in // the first week of the next year; // - in all the other cases, we compute the number of the week // normally. if (yearDay < start) return week(31, 12, year-1); if (yearDay > lastSunday) return (year + 1)*100 + 1; return year*100 + ((yearDay - start) / 7 + 1); } |
(the code is in this file). To disable the printout of the ISO week numbers, find in the code the following line:
/weeksneeded 1 def
and change the "1" to a "0". That's all.
The last part is a simple recreational exercise in programming; two procedures that perform the conversions between Julian Day numbers and the French Revolutionary Calendar coined in 1793. A conversion of a Gregorian date to or from a French Revolutionary date may be always done after an intermediate conversion to a Julian Day number. Some notes: the French Revolutionary Calendar started on Julian Day 2375840; its first day, the 1 Vendemiaire an I, corresponds to September 22, 1792 (altough the Calendar has been introduced in November 24, 1793). It was finally abolished by Napoléon on January 1, 1806 — or, in fact, at the midnight of 10 Nivôse an XIV.
More precisely, the calendar definition used in the code corresponds to a variant version proposed by its creator, Gilbert Romme, in 1795; that contains the following rule (the one proposed by Herschel and described in this section):
With this definition, as already noted, in a 4000 years cycle there are exactly 1460969 days (with a mean year length of 365.24225 days); with the original definition adopted by the National Convention in 1793, the first day of every Revolutionary Year was the day of the Autumnal Equinox in Paris; and there was no definite rule for leap years.
The Revolutionary year was divided in 12 months of 30 days each, and followed by 5 additional days (6 in the the leap years); the month names (Vendémiaire, Brumaire, Frimaire; Nivôse, Pluviôse, Ventôse; Germinal, Floréal, Prairial; Messidor, Thermidor, Fructidor) were coined by the french poet Fabre d'Églantine. The 5 (or 6) additional days were holidays dedicated to Virtue, Genius, Labour, Opinion, Rewards and (the leap day) to the Revolution itself (Fêtes de la Vertu, du Génie, du Travail, de l'Opinion, des Récompenses, de la Révolution).
The 12 months were additionally divided in 3 décades each one 10 days long, with 9 working days and a final holiday (and that made the calendar unpopular, since the previous one had an holiday every 6 working days). Instead of each day having associated a Saint as in the Catholic church's calendar, each day had a plant, a tool or an animal associated with it.
The procedures accept (or return), for the month, a number in the interval from 1 to 13; the fake 13th month corresponds to the group of 5 (or, in the leap years, 6) additional days that follow the "normal" year (12 months of 30 days each, i.e. 360 days). Here they are:
#include <stdexcept> const long frenchEpoch = 2375840; const double daysPerFrenchYear = 1460969.0 / 4000.0; // The names of the 5 (or 6) additional days at the end of the normal // calendar (that has 12 months of 30 days each, or 360 days) static const char * sansCulNames [] = { "Invalide", "Jour de la Vertu", "Jour du Genie", "Jour du Travail", "Jour de la Raison", "Jour des Recompenses", "Jour de la Revolution" }; // The names of the 12 months (coined by Fabre d'Eglantine) static const char * frenchMonthsNames [] = { "Invalide", "Vendemiaire", "Brumaire", "Frimaire", "Nivose", "Pluviose", "Ventose", "Germinal", "Floreal", "Prairial", "Messidor", "Thermidor", "Fructidor" }; long french2jul( int fd, int fm, int fy ) { // From Edward M. Reingold and Nachum Dershowitz - "Calendrical // Calculations: The Millennium Edition" (Cambridge University // Press, 2001) - page 238 // // Essentially, all the days before the given date are summed to the // Julian Day number corresponding to the first year of the French // Revolutionary Calendar. if (fy < 1) { throw std::domain_error("Before the French Revolution"); } long result = frenchEpoch - 1; result += 365 * --fy; result += fy / 4; result -= fy / 100; result += fy / 400; result -= fy / 4000; result += 30 * --fm; result += fd; return result; } void jul2french( long jd, int & fd, int & fm, int & fy ) { // From Edward M. Reingold and Nachum Dershowitz - "Calendrical // Calculations: The Millennium Edition" (Cambridge University // Press, 2001) - page 238 // // A first approximation of the year in the French Revolutionary // Calendar is found, and refined; then, the precise date is // computed from the Julian Day number corresponding to the start of // the current Revolutionary year. if (jd < frenchEpoch) { throw std::domain_error("Before the French Revolution"); } long approx = jd - frenchEpoch + 2; fy = static_cast<int>(static_cast<double>(approx)/daysPerFrenchYear) + 1; long startOfYear = french2jul(1, 1, fy); if (jd < startOfYear) { startOfYear = french2jul(1, 1, --fy); // We were wrong by 1 year... } fm = (jd -= startOfYear) / 30; fd = jd - 30 * fm++ + 1; } |
(the code is in this file).
As you may have noticed, the years in the French Revolutionary Calendar appear in writing as Roman numerals; as a bonus, the following C++ snippet contains a procedure that shows integer numbers using Roman numerals together with a simple test program (the source code is here).
#ifdef TEST_PROGRAM #include <iostream> #include <cstdlib> #endif // TEST_PROGRAM #include <string> static const char rc[] = { 'I', 'V', 'X', 'L', 'C', 'D', 'M' }; static const int rMin(1), rMax(3999); std::string roman( int dec ) { static const size_t bufSize = 24; // Returns a std::string containing the representation, in Roman // numerals, of the decimal number "dec". "rMin" and "rMax" define // the range we can represent with the numerals defined in "rc". if (dec < rMin || dec > rMax) return "Out of range"; char buf[bufSize]; const char * pc = rc; char * last = buf + bufSize; *--last = '\0'; do { if (int digit = dec % 10) { int ni = digit % 5; if (ni == 4) { if (digit == 4) *--last = *(pc+1); else *--last = *(pc+2); *--last = *pc; } else { switch (ni) { case 0: *--last = *(pc+1); break; case 1: case 2: case 3: while (ni--) *--last = *pc; if (digit > 4) *--last = *(pc+1); break; } } } dec /= 10; pc += 2; } while (dec); return last; } #ifdef TEST_PROGRAM int main( int argc, char * argv[] ) { if (argc == 1) { std::cerr << "Usage:\t" << argv[0] << " i1 [ ,i2 [ ,i3 [ " << "... ]]]\n\n\tConverts the command line " << "arguments (numbers in the range " << rMin << "..." << rMax << ")\n\tto Roman numerals.\n"; } else { while (--argc) { int i = std::atoi(*++argv); std::cout << i << "\t==> " << roman(i) << std::endl; } } } #endif // TEST_PROGRAM |
The file main.cpp is a quick-and-dirty test program for the C++ snippets listed before. Be aware that the test of the user input is quite rudimentary; do not type numbers out of range or non-numeric characters.
Revised August 30, 2006 - MLO (loreti at pd dot infn dot it) - This page has been hand-made with
Any comment? Click here.