Cal
January 1, 2010
The Standard Prelude provides functions julian
, gregorian
and today
to handle the date arithmetic, so this is primarily an exercise in formatting. A month is 8 rows high and 22 columns wide (including empty columns at left and right to possibly hold an angle bracket for the current date); a three-month calendar is 66 columns wide and a twelve-month calendar is 35 rows long:
January 2010 Su Mo Tu We Th Fr Sa 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Many of the functions need to know the current year, month and day. We store them in global variables year
, month
and day
, and set them once in the call to cal
:
(define year #f)
(define month #f)
(define day #f)
(define (today-set!)
(let ((t (today)))
(let-values (((y m d) (gregorian t)))
(set! year y)
(set! month m)
(set! day d))))
We begin at the end with the function that parses the arguments and prints the desired calendar:
(define (cal . args)
(today-set!)
(cond ((null? args) (display-month year month))
((and (number? (car args)) (= (car args) -3))
(display-three
(if (= month 1)
(make-month (- year 1) 12)
(make-month year (- month 1)))
(make-month year month)
(if (= month 12)
(make-month (+ year 1) 1)
(make-month year (+ month 1)))))
((= (length args) 1) (display-year (car args)))
(else (display-month (cadr args) (car args)))))
There are three functions that print one month, three months, and twelve months. We’ll be representing a month as a string with 22 × 8 = 176 characters (there are twenty-two print columns and eight rows in a month) and slicing it as needed for display. Here is the function to print a single month:
(define (display-month y m)
(let ((month-string (make-month y m)))
(do ((i 0 (+ i 1))) ((= i 8))
(display (substring month-string (* i 22) (* (+ i 1) 22)))
(newline))))
The function to print three months side-by-side is similar:
(define (display-three m1 m2 m3)
(do ((i 0 (+ i 1))) ((= i 8))
(display (substring m1 (* i 22) (* (+ i 1) 22)))
(display (substring m2 (* i 22) (* (+ i 1) 22)))
(display (substring m3 (* i 22) (* (+ i 1) 22)))
(newline)))
To print a year, we call the function to print three months side-by-side four times:
(define (display-year y)
(display-three (make-month y 1) (make-month y 2) (make-month y 3))
(newline)
(display-three (make-month y 4) (make-month y 5) (make-month y 6))
(newline)
(display-three (make-month y 7) (make-month y 8) (make-month y 9))
(newline)
(display-three (make-month y 10) (make-month y 11) (make-month y 12)))
All that’s left is the function that forms a month-string for a given month. It’s a complicated function, dealing with both the logical calendar and the physical representation on the printed page.
On the logical side, variable n is the number of days in the month, variable b is the number of “extra” days on the calendar before the start of the month, and variable e is the number of extra days on the calendar after the end of the month. N is calculated by subtracting the julian number at the beginning of the current month from the julian number at the beginning of the succeeding month. B is calculated from the day of the week by taking the julian number at the beginning of the current month modulo seven. E is calculated from the relationship b + n + e = 42, which is 6 rows times 7 days per week.
On the physical side, variable r is the number of the current row (the month and year name are on row 0, the day names are on row 1, and the six rows of the calendar are rows 2 through 7), variable c is the number of the “cell” across the calendar from 0 through 6, and variable s is the offset into the string of the left-most digit position of a particular calendar cell. S is calculated as r × 22 + c × 3 + 1.
The logical and physical sides of the calendar are related by two equations that calculate r and c given b and i, where i indexes through the days of the month from 1 to n. R is given by (b + i – 1) / 7 + 2, where the division is integer division, and c is given by (b + i – 1) modulo 7. As a practical matter, we don’t really need e, and it is easier to calculate s directly from b and i than to make the intermediate calculations of r and c. All of these formulas can be derived with a little bit of effort from the basic structure of a calendar month.
Make-month
takes a month and year and returns a month-string suitable for printing by the three display functions. It’s not hard, as long as you keep the equations described above firmly in mind:
(define (make-month y m)
(define (s b i)
(+ (* (+ (quotient (+ b i -1) 7) 2) 22)
(* (modulo (+ b i -1) 7) 3) 1))
(define (r-just n)
(if (< n 10) (string-append " " (number->string n)) (number->string n)))
(let* ((str (make-string 176 #\space))
(j (julian y m 1))
(n (- (julian (if (= m 12) (+ y 1) y) (if (= m 12) 1 (+ m 1)) 1) j))
(b (modulo (+ j 1) 7)))
(string-put! str 0
(center 22 (string-append (month-name m) " " (number->string y))))
(string-put! str 22 " Su Mo Tu We Th Fr Sa ")
(do ((i 1 (+ i 1))) ((> i n))
(string-put! str (s b i) (r-just i))
(when (and (= y year) (= m month) (= i day))
(string-set! str (- (s b i) 1) #\<)
(string-set! str (+ (s b i) 2) #\>)))
str))
We need a few helper functions. String-put!
is like string-set!
, but takes a string instead of a single character. Month-name
returns a spelled-out month name given a month number. Center
centers a substring in a larger string.
(define (string-put! str k substr)
(do ((i 0 (+ i 1)) (k k (+ k 1)))
((= i (string-length substr)) str)
(string-set! str k (string-ref substr i))))
(define (month-name m)
(vector-ref (vector "January" "February" "March" "April" "May" "June"
"July" "August" "September" "October" "November" "December") (- m 1)))
(define (center wid str)
(let* ((len (string-length str))
(lpad (quotient (- wid len) 2))
(rpad (- wid len lpad)))
(if (< wid len)
(substr str 0 wid)
(string-append
(make-string lpad #\space) str (make-string rpad #\space)))))
And that’s it. Here is the calendar for 2010:
> (cal 2010) January 2010 February 2010 March 2010 Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa 1 2 1 2 3 4 5 6 1 2 3 4 5 6 3 4 5 6 7 8 9 7 8 9 10 11 12 13 7 8 9 10 11 12 13 10 11 12 13 14 15 16 14 15 16 17 18 19 20 14 15 16 17 18 19 20 17 18 19 20 21 22 23 21 22 23 24 25 26 27 21 22 23 24 25 26 27 24 25 26 27 28 29 30 28 28 29 30 31 31 April 2010 May 2010 June 2010 Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa 1 2 3 1 1 2 3 4 5 4 5 6 7 8 9 10 2 3 4 5 6 7 8 6 7 8 9 10 11 12 11 12 13 14 15 16 17 9 10 11 12 13 14 15 13 14 15 16 17 18 19 18 19 20 21 22 23 24 16 17 18 19 20 21 22 20 21 22 23 24 25 26 25 26 27 28 29 30 23 24 25 26 27 28 29 27 28 29 30 30 31 July 2010 August 2010 September 2010 Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa 1 2 3 1 2 3 4 5 6 7 1 2 3 4 4 5 6 7 8 9 10 8 9 10 11 12 13 14 5 6 7 8 9 10 11 11 12 13 14 15 16 17 15 16 17 18 19 20 21 12 13 14 15 16 17 18 18 19 20 21 22 23 24 22 23 24 25 26 27 28 19 20 21 22 23 24 25 25 26 27 28 29 30 31 29 30 31 26 27 28 29 30 October 2010 November 2010 December 2010 Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa 1 2 1 2 3 4 5 6 1 2 3 4 3 4 5 6 7 8 9 7 8 9 10 11 12 13 5 6 7 8 9 10 11 10 11 12 13 14 15 16 14 15 16 17 18 19 20 12 13 14 15 16 17 18 17 18 19 20 21 22 23 21 22 23 24 25 26 27 19 20 21 22 23 24 25 24 25 26 27 28 29 30 28 29 30 26 27 28 29 30 31 31
You can run the program at http://programmingpraxis.codepad.org/5T41NUut.
[…] today’s Programming Praxis exercise we have to implement the Unix utility cal, which prints calendars. […]
My Haskell solution (see http://bonsaicode.wordpress.com/2010/01/01/programming-praxis-cal/ for a version with comments):
There’s a bug here: compare the output of your cal with the standard one (either BSD or GNU) on September 1752 or any earlier date.
“Give us back our eleven days!”
C++ style
#include
#include
#include
using namespace std;
const int MONTHS_IN_YEAR = 12;
const int DAYS_IN_WEEK = 7;
const int MAX_DAYS_IN_MONTH = 31;
const int MIN_DAYS_IN_MONTH = 28;
const int LEAP_YEAR_FACTOR = 4;
const int DAY_WIDTH = 3;
const int JAN = 1;
const int FEB = 2;
const int MAR = 3;
const int APR = 4;
const int MAY = 5;
const int JUN = 6;
const int JUL = 7;
const int AUG = 8;
const int SEP = 9;
const int OCT = 10;
const int NOV = 11;
const int DEC = 12;
void PrintMonthHeader(int month);
string MonthString(int month);
void PrintMonthBody(int year, int month, int& day);
void PrintMonthDates(int& day, int lastday);
void PrintSelectedMonths(int year, int start_month,
int day, int total_months);
int main()
{
int month, totalMonths, startDay, year;
cout <> year;
while (year != 0)
{
cout <> month;
cout <> totalMonths;
cout << "What day of the week does the starting month fall on?\n";
cout <> startDay;
PrintSelectedMonths(year, month, startDay, totalMonths);
cout <> year;
}
return 0;
}
//———————————————————————–
// This function uses the integer month to print out that months name.
// It also prints out the one letter abbreviations for Sunday
// through Saturday and the underscores under them.
// Parameters: (in)
//———————————————————————–
void PrintMonthHeader(int month)
{
cout << "\n" << " " << MonthString(month) << '\n';
cout << " S M T W T F S\n";
cout < 0)
{
if (day == DAYS_IN_WEEK)
daySpaces = 0;
else
{
cout << " ";
daySpaces–;
}
}
while (daysInMonth <= lastday)
{
if (day % DAYS_IN_WEEK == 0 && daysInMonth != 1)
{
cout << "\n";
day = 0;
}
cout << setw(DAY_WIDTH) << daysInMonth;
daysInMonth++;
day++;
}
cout << endl;
return;
}
//———————————————————————–
// This function prints out the calendar for the given number
// of months (total_months), the given year, using the
// start_month and starting day (day). The function repeatedly
// calls PrintMonthHeader and PrintMonthBody.
// Parameters: (in, in, in, in)
//———————————————————————–
void PrintSelectedMonths(int year, int start_month,
int day, int total_months)
{
int monthCount = 0;
cout << "\n\n" << " " << year << "\n" ;
while(monthCount < total_months)
{
PrintMonthHeader(start_month);
PrintMonthBody(year, start_month, day);
if (start_month % 12 == 0 && total_months != 12)
{
year++;
cout << "\n\n\n" << " " << year << "\n" ;
monthCount = 1;
start_month = 0;
}
monthCount++;
start_month++;
}
return;
}