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.