Create a routine that will generate a text calendar for any year.
Test the calendar by generating a calendar for the year 1969, on a device of the time.
Choose one of the following devices:

 A line printer with a width of 132 characters.
 An IBM 3278 model 4 terminal (80×43 display with accented characters). Target formatting the months of the year to fit nicely across the 80 character width screen. Restrict number of lines in test output to 43.


(Ideally, the program will generate well-formatted calendars for any page width from 20 characters up.)

Kudos (κῦδος) for routines that also transition from Julian to Gregorian calendar.

This task is inspired by Real Programmers Don't Use PASCAL by Ed Post, Datamation, volume 29 number 7, July 1983.

THE REAL PROGRAMMER'S NATURAL HABITAT
"Taped to the wall is a line-printer Snoopy calender for the year 1969."

For further Kudos see task CALENDAR, where all code is to be in UPPERCASE.

For economy of size, do not actually include Snoopy generation in either the code or the output, instead just output a place-holder.



Related task

   Five weekends


import qualified Data.Text as T
import Data.Time
import Data.Time.Calendar
import Data.Time.Calendar.WeekDate
import Data.List.Split (chunksOf)
import Data.List
 
data Day = Su | Mo | Tu | We | Th | Fr | Sa
           deriving (Show, Eq, Ord, Enum)
 
data Month = January | February | March
           | April   | May      | June
           | July    | August   | September
           | October | November | December
             deriving (Show, Eq, Ord, Enum)
 
monthToInt :: Month -> Int
monthToInt = (+ 1) . fromEnum
 
dayFromDate :: Integer -> Month -> Int -> Int
dayFromDate year month day = day' `mod` 7
    where (_, _, day') = toWeekDate $ fromGregorian year (monthToInt month) day
 
nSpaces :: Int -> T.Text
nSpaces n = T.replicate n (T.pack " ")
 
space :: T.Text
space = nSpaces 1
 
calMarginWidth = 3
 
calMargin :: T.Text
calMargin = nSpaces calMarginWidth
 
calWidth = 20
 
listMonth :: Integer -> Month -> [T.Text]
listMonth year month = [monthHeader, weekHeader] ++ weeks'
    where
      monthHeader = (T.center calWidth ' ') . T.pack $ show month
 
      weekHeader = (T.intercalate space) $ map (T.pack . show) [(Su)..]
 
      monthLength = toInteger $ 
                    gregorianMonthLength year $
                    monthToInt month
 
      firstDay = dayFromDate year month 1
 
      days = replicate firstDay (nSpaces 2) ++
             map ((T.justifyRight 2 ' ') . T.pack . show) [1..monthLength]
 
      weeks = map (T.justifyLeft calWidth ' ') $
              map (T.intercalate space) $
              chunksOf 7 days
 
      weeks' = weeks ++ replicate (6 - length weeks) (nSpaces calWidth)
 
listCalendar :: Integer -> Int -> [[[T.Text]]]
listCalendar year calColumns = (chunksOf calColumns) . (map (listMonth year)) $
                               enumFrom January
 
calColFromCol :: Int -> Int
calColFromCol columns = c + if r >= calWidth then 1 else 0
    where (c, r) = columns `divMod` (calWidth + calMarginWidth)
 
colFromCalCol :: Int -> Int
colFromCalCol calCol = calCol * calWidth + ((calCol - 1) * calMarginWidth)
 
center :: Int -> String -> String
center i a = T.unpack . (T.center i ' ') $ T.pack a
 
printCal :: [[[T.Text]]] -> IO ()
printCal = mapM_ f where
  f c = mapM_ (putStrLn . T.unpack) rows
    where rows = map (T.intercalate calMargin) $ transpose c
 
printCalendar :: Integer -> Int -> IO ()
printCalendar year columns =
    if columns < 20
    then putStrLn $ "Cannot print less than 20 columns"
    else do
      putStrLn $ center columns' "[Maybe Snoopy]"
      putStrLn $ center columns' $ show year
      putStrLn ""
      printCal $ listCalendar year calcol'
    where
      calcol' = calColFromCol columns
 
      columns' = colFromCalCol calcol'