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.

Pages: 1 2

5 Responses to “Cal”

  1. […] today’s Programming Praxis exercise we have to implement the Unix utility cal, which prints calendars. […]

  2. Remco Niemeijer said

    My Haskell solution (see http://bonsaicode.wordpress.com/2010/01/01/programming-praxis-cal/ for a version with comments):

    import Data.List
    import Data.List.Split
    import qualified Data.Text as T
    import Data.Time
    import System.Environment
    import System.Locale
    
    days :: Integer -> Int -> [Day]
    days y m = map (fromGregorian y m) [1..gregorianMonthLength y m]
    
    fmt :: FormatTime t => String -> t -> String
    fmt = formatTime defaultTimeLocale
    
    monthCal :: Integer -> Int -> String
    monthCal y m = unlines $ (T.unpack . (T.center 20 ' ') . T.pack .
        fmt "%B %Y" $ fromGregorian y m 1) : "Su Mo Tu We Th Fr Sa" :
        (map unwords . take 6 . chunk 7 $
            replicate (read . fmt "%w" . head $ days y m) "  " ++
            map (fmt "%e") (days y m) ++ repeat "  ")
    
    showCal :: [String] -> IO ()
    showCal = putStrLn . unlines . map (unlines . map
                (intercalate "  ") . transpose . map lines) . chunk 3
    
    surround :: UTCTime -> [String]
    surround d = map ((\(y, m, _) -> monthCal y m) . toGregorian .
                   (`addGregorianMonthsRollOver` utctDay d)) [-1..1]
    
    main :: IO ()
    main = do args <- getArgs
              now  <- getCurrentTime
              let (curYear, curMonth) = read $ fmt "(%Y,%m)" now
              case args of
                  [y,m]  -> showCal [monthCal (read y) (read m)]
                  ["-3"] -> showCal $ surround now
                  [y]    -> showCal $ map (monthCal $ read y) [1..12]
                  []     -> showCal [monthCal curYear curMonth]
                  _      -> error "Invlaid parameters"
    
  3. johnwcowan said

    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!”

  4. adactuslatem said

    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;
    }

  5. Mike said
    import calendar
    import datetime as dt
    import itertools as it
    import re
    
    calendar.setfirstweekday(calendar.SUNDAY)
    
    def onemonth( month=None, year=None ):
        today = dt.date.today()
        month = month or today.month
        year = year or today.year
        
        mstring = re.sub( r"\n", r" \n ",  calendar.month( year, month ) )
    
        if year == today.year and month == today.month:
            mstring = re.sub( r" (%2s) "%today.day, r"[\1]", mstring )
    
        return mstring.splitlines()
    
    
    def threemonth( month=None, year=None ):
        today = dt.date.today()
        month = month or today.month
        year = year or today.year
    
        m1 = onemonth( month-1, year ) if month > 1 else onemonth( 12, year-1 )
        m2 = onemonth( month,   year )
        m3 = onemonth( month+1, year ) if month < 12 else onemonth( 1, year+1 )
    
        fmt = "{0:22}{1:22}{2:22}".format
        return it.starmap( fmt, it.izip_longest( m1, m2, m3, fillvalue='' ) )
    
    
    def twelvemonths(year=None):
        return it.chain( *( threemonth( m, year ) for m in (2, 5, 8, 11) ) )
    
    
    def cal( *args ):
        if len( args ) == 1:
            year = int( args[0] )
            if year == -3: 
                rows = threemonth()
            else:
                rows = twelvemonths( year )
        else:
            rows = onemonth( *map( int, args ) ) 
    
        return '\n'.join( rows )
    
    if __name__ == '__main__':
        from sys import argv
        print cal( *argv[1:] )
    
    

Leave a comment