The Big Book of Small EK9 Programs

Adapted from Al Sweigart's The Big Book of Small Python Projects: 81 Easy Practice Programs (No Starch Press, 2021; copyright © 2021 Al Sweigart), which he makes freely available under a Creative Commons Attribution-NonCommercial-ShareAlike licence at inventwithpython.com/bigbookpython. Changes were made: each program was ported from Python to EK9 and remodelled in EK9's idioms.

Project 8 — Calendar Maker

Calendar Maker draws a single month as a blank, ruled calendar grid: a title row for the month and year, a header of day names starting with Sunday, and a boxed cell for every day of the month with three blank rows underneath each one for notes — the kind of page you might print and write a month's plans onto by hand.

Adapted from project 8 of Al Sweigart's The Big Book of Small Python Projects (No Starch Press, 2021), under CC BY-NC-SA 4.0.

A sample run

The year and month are program arguments (args.txt holds 2026 then 8); there is no stdin to answer:

ek9 calendar-maker.ek9 2026 8
                                 August 2026                                  
   Sunday     Monday    Tuesday   Wednesday   Thursday    Friday    Saturday  
+----------+----------+----------+----------+----------+----------+----------+
|          |          |          |          |          |          | 1        |
|          |          |          |          |          |          |          |
|          |          |          |          |          |          |          |
|          |          |          |          |          |          |          |
+----------+----------+----------+----------+----------+----------+----------+
| 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        |          |          |          |          |          |
|          |          |          |          |          |          |          |
|          |          |          |          |          |          |          |
|          |          |          |          |          |          |          |
+----------+----------+----------+----------+----------+----------+----------+

The program

#!ek9
defines module bigbook.calendar

  defines constant

    CellWidth <- 10
    NoteRows <- 3
    Sunday <- 7
    DaysInWeek <- 7
    OneWeek <- P7D

  defines type

    List of Date

  defines class

    //THE OUTPUT: everything the reader sees goes through here, worded by Say. The year and
    //month are program arguments, so there is no LineInput.
    Console
      stdout as Stdout: Stdout()
      say as Say: Say("en")

      Console()
        -> words as Say
        say: words

      //The calendar is computed, not worded: it is shown as the game drew it.
      showCalendar()
        -> calendar as List of String
        for line in calendar
          stdout.println(line)

      showUsage()
        stdout.println(say.usage())

      override operator ? as pure
        <- rtn as Boolean: true

  defines function

    //THE GAME: the calendar, drawn as lines, with no output or words.

    //Date.dayOfWeek() runs Monday = 1 to Sunday = 7, and the calendar's weeks start on Sunday.
    sundayOnOrBefore()
      -> date as Date
      <- rtn as Date: Date(date)
      while rtn.dayOfWeek() <> Sunday
        rtn--

    //Each Date is a new object: ++ changes a Date in place, so adding `day` itself would
    //put seven references to one changing Date in the list.
    weekFrom()
      -> sunday as Date
      <- rtn as List of Date: List() of Date
      day <- Date(sunday)
      for weekday in 1 ... DaysInWeek
        rtn += Date(day)
        day++

    //A whole cell of one character: a rule of '-' or a blank interior of ' '. A String
    //literal cannot take a method call ("".rightPadded does not parse), so it is named first.
    cellOf() as pure
      -> fill as Character
      <- rtn as String: String()
      nothing <- ""
      rtn: nothing.rightPadded(CellWidth, fill)

    //A day from a neighbouring month is left blank.
    dayCell() as pure
      ->
        date as Date
        month as Integer
      <- rtn as String: cellOf(' ')

      if date.month() == month
        dayNumber <- $date.day()
        rtn: dayNumber.leftPadded(2).rightPadded(CellWidth)

    dayCells()
      ->
        week as List of Date
        month as Integer
      <- rtn as List of String: List() of String

      for date in week
        rtn += dayCell(date, month)

    //One line across the calendar: a border, then each cell followed by a border. rtn starts
    //as a copy of border because += changes a String in place, and border is the caller's.
    ruledLine()
      ->
        cells as List of String
        border as String
      <- rtn as String: String(border)

      for cell in cells
        rtn += `${cell}${border}`

    centredName() as pure
      -> name as String
      <- rtn as String: name.center(CellWidth)

    //The lines of the month that firstOfMonth starts. The month and day names are the
    //calendar's data, as its dates are.
    calendarLines()
      -> firstOfMonth as Date
      <- rtn as List of String: List() of String

      month <- firstOfMonth.month()
      monthNames <- ["January", "February", "March", "April", "May", "June",
        "July", "August", "September", "October", "November", "December"]
      dayNames <- ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]

      dashes <- List() of String
      blanks <- List() of String
      for name in dayNames
        dashes += cellOf('-')
        blanks += cellOf(' ')
      separator <- ruledLine(dashes, "+")
      noteRow <- ruledLine(blanks, "|")

      title <- `${monthNames.getOrDefault(month - 1, "")} ${firstOfMonth.year()}`
      rtn += title.center(length separator)
      header <- cat dayNames | map with centredName | collect as List of String
      rtn += ruledLine(header, " ")

      //As in the book: draw a week, move to the next Sunday, and stop once that Sunday is
      //in the following month.
      weekStart <- sundayOnOrBefore(firstOfMonth)
      do
        rtn += separator
        rtn += ruledLine(dayCells(weekFrom(weekStart), month), "|")
        for row in 1 ... NoteRows
          rtn += noteRow
        weekStart += OneWeek
      while weekStart.month() == month
      rtn += separator

  defines function

    //THE language of the words, copied unchanged from lib/Language.ek9: BIGBOOK_LANG when it
    //is set (fr, zh, ...), otherwise en.
    language()
      <- rtn as String: EnvVars().get("BIGBOOK_LANG") ?: "en"

  //THE WORDS: every word the reader sees. The calendar is not here: the game draws it.
  defines text for "en"

    Say

      usage()
        "Give a year and a month from 1 to 12, for example: 2026 8"

  defines program

    //Calendar Maker (Big Book project 8). Draws the month the year and month arguments name as a
    //ruled grid, Sunday first, with room under each day for notes. The game decides; the Console
    //shows, in the reader's words.
    CalendarMaker()
      ->
        year as Integer
        month as Integer

      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))

      if firstOfMonth <- Date(year, month, 1)
        console.showCalendar(calendarLines(firstOfMonth))
      else
        console.showUsage()

How the port works

The original's getCalendarFor() already hands the calendar back as one string for main() to print. The port goes on from there and keeps three things apart, as every port in this book does. calendarLines(firstOfMonth) is the game: it draws the month and hands back its lines, a List of String, without printing anything. The Console shows them as they were drawn (showCalendar), or shows the usage line (showUsage) when there is no such month; the year and month are program arguments, so it holds no LineInput. Say has only that usage line, because everything else on the page is computed. The month and day names are not words for Say either: they are the calendar's data, as its dates are, measured into cells and centred with the grid, so they stay in the game. The program is the flow, and chooses the words with Say(language()) ?: Say("en").

The month's weeks are drawn with a do ... while loop, EK9's post-condition loop: the body — draw a week, then its three blank note rows — always runs at least once, and only afterwards is the Sunday that was just reached tested against the month. That is precisely the shape of the original's while True with a break once the week has moved past the month, expressed directly rather than through an infinite loop with an exit hidden partway through the body.

Date(year, month, 1) is unset for a month and day combination that cannot exist (for example month 13), so the single guard if firstOfMonth <- Date(year, month, 1) stands in for the whole validation loop the original needs. The year and month also arrive as typed Integer program arguments, so by the time the program body runs the compiler has already proven they are numbers — only the month range is left for the guard to catch.

Two places need an explicit copy because EK9 objects alias on assignment rather than copying: weekFrom builds Date(day) — a brand-new object — on every pass of its loop, because day++ changes day in place, and storing the same day directly into the result list seven times would leave seven references to one date that keeps moving. ruledLine starts its accumulator as String(border) rather than aliasing the caller's border string directly, because += would then grow the caller's own string as a side effect. Both are the same "this is an object, not a primitive" rule from different angles — one on a Date, one on a String.

Padding and centring come straight from the stdlib — leftPadded, rightPadded and center — so there is no hand-rolled padding helper anywhere in the port, and weekStart += OneWeek adds a P7D duration literal to a Date directly rather than counting seven days by hand. The cell builders cellOf, dayCell and centredName are as pure; there is no break, continue or return statement anywhere in the port.