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 2 — Birthday Paradox
Birthday Paradox is a probability demonstration dressed up as a program. It asks how many people should be in an imaginary room, generates that many random birthdays, shows them, and says whether any two happen to fall on the same day. Then it runs the same experiment one hundred thousand times and reports what fraction of those rooms turned up a shared birthday — which, for surprisingly small groups, is a lot higher than most people guess.
Adapted from project 2 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 birthdays are random, so the golden run fixes them by setting BIGBOOK_SEED=2 (from
seed.txt) before answering the one prompt from input.txt, a group size of 23:
23
BIGBOOK_SEED=2 ek9 birthdayparadox.ek9 < input.txt
The typed group size is never echoed back; the program's own lines are everything below:
Birthday Paradox, by Al Sweigart
How many birthdays shall I generate? (Max 100)
Here are 23 birthdays:
Apr 4, Nov 19, Oct 29, Dec 12, Aug 29, May 11, Aug 19, Nov 4, Feb 9, Sep 19, Jul 25, Apr 14, Oct 6, Dec 22, Mar 19, May 16, Feb 19, Jan 15, Jun 24, Jul 13, Jun 25, May 28, May 28
In this simulation, multiple people have a birthday on May 28
Generating 23 random birthdays 100000 times...
In 100000 simulations of 23 people, a matching birthday happened 50744 times.
That means 23 people have a 50.74 % chance of a shared birthday.
The program
#!ek9 defines module bigbook.birthdayparadox defines constant Trials <- 100000 MaxPeople <- 100 LastDay <- 364 StartOfYear <- 2001-01-01 PercentagePlaces <- 2 defines type List of Date defines class //THE input idiom of this book, copied unchanged from lib/LineInput.ek9 (the reasons are //there): read with next(), and every input loop also stops when ended() is true. LineInput stdin as Stdin: Stdin() inputEnded as Boolean: false next() <- rtn as String: stdin.next() if not rtn? inputEnded: true ended() as pure <- rtn as Boolean: Boolean(inputEnded) override operator ? as pure <- rtn as Boolean: true //THE INPUT AND OUTPUT: everything the player sees or types goes through here, worded by Say. Console stdout as Stdout: Stdout() input as LineInput: LineInput() say as Say: Say("en") Console() -> words as Say say: words showIntroduction() stdout.println(say.introduction()) //Asks until the answer is a group size; unset only if the input ends first. nextGroupSize() <- rtn as Integer: Integer() stdout.println(say.askGroupSize()) while not rtn? and not input.ended() entry <- Integer(input.next()) if isGroupSize(entry) rtn: entry else stdout.println(say.notAGroupSize()) showBirthdays() -> birthdays as List of Date stdout.println(say.hereAre(length birthdays)) labels <- List() of String for birthday in birthdays labels += labelOf(birthday) line <- cat labels | join with withComma | collect as String stdout.println(line) showShared() -> birthday as Date stdout.println(say.shared(labelOf(birthday))) showNoneShared() stdout.println(say.noneShared()) showSimulating() -> count as Integer stdout.println(say.simulating(count)) showResults() -> count as Integer matchCount as Integer chance as Float stdout.println(say.results(count, matchCount, chance)) private labelOf() -> birthday as Date <- rtn as String: say.birthday(monthAbbreviation(birthday), birthday.day()) override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: the experiment, with no input or output. isGroupSize() as pure -> entry as Integer <- rtn as Boolean: false if entry? rtn: entry >= 1 and entry <= MaxPeople //The year does not matter as long as every birthday uses the same one, so a birthday //is a real Date: the start of the year plus a random number of days, as in the book. //The Duration is built from its text, P<n>D, which keeps it as n days: P1D * n is //normalised into 30-day months today and lands on the wrong date (P345D is Dec 12, //P1D * 345 is Dec 16). randomBirthdays() -> count as Integer rng as Random <- rtn as List of Date: List() of Date for person in 1 ... count rtn += StartOfYear + Duration(`P${rng.next(0, LastDay)}D`) //The month names are data, as the book's MONTHS tuple is. monthAbbreviation() as pure -> birthday as Date <- rtn as String: String() months <- ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] rtn: months.getOrDefault(birthday.month() - 1, "") //uniq drops repeated dates, so a group with a shared birthday comes out shorter. hasSharedBirthday() as pure -> birthdays as List of Date <- rtn as Boolean: length distinctBirthdays(birthdays) < length birthdays distinctBirthdays() as pure -> birthdays as List of Date <- rtn as List of Date: cat birthdays | uniq | collect as List of Date //The book's getMatch: the earliest birthday that occurs again later in the list, or //unset when every birthday is different. firstSharedBirthday() -> birthdays as List of Date <- rtn as Date: Date() later <- List() of Date later += birthdays for birthday in birthdays later -= birthday if not rtn? and later contains birthday rtn: birthday matchingGroups() -> count as Integer rng as Random <- rtn as Integer: 0 for trial in 1 ... Trials if hasSharedBirthday(randomBirthdays(count, rng)) rtn += 1 //The percentage of the groups that shared a birthday, to two places. sharedChance() as pure -> matchCount as Integer <- rtn as Float: Float() percentage <- Float(matchCount) * 100.0 / Float(Trials) rtn: percentage.round(PercentagePlaces) defines function withComma() as pure -> left as String right as String <- rtn as String: `${left}, ${right}` //THE source of randomness, copied unchanged from lib/RandomSource.ek9: SystemRandom(), or //SeededRandom(seed) when BIGBOOK_SEED holds a number (run-and-diff.sh sets it from seed.txt). randomSource() <- rtn as Random: SeededRandom(Integer(EnvVars().get("BIGBOOK_SEED"))) ?: SystemRandom() //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 player sees. defines text for "en" Say introduction() "Birthday Paradox, by Al Sweigart" askGroupSize() `How many birthdays shall I generate? (Max ${MaxPeople})` notAGroupSize() `Enter a number between 1 and ${MaxPeople}.` hereAre() -> count as Integer `Here are ${count} birthdays:` birthday() -> month as String day as Integer `${month} ${day}` shared() -> birthday as String `In this simulation, multiple people have a birthday on ${birthday}` noneShared() "In this simulation, there are no matching birthdays." simulating() -> count as Integer `Generating ${count} random birthdays ${Trials} times...` results() -> count as Integer matchCount as Integer chance as Float `In ${Trials} simulations of ${count} people, a matching birthday happened ${matchCount} times. That means ${count} people have a ${chance} % chance of a shared birthday.` defines program //Birthday Paradox (Big Book project 2). A Monte Carlo estimate of how likely a group of //people is to share a birthday. The game decides; the Console asks and tells, in the player's //words. The golden replays a seeded game (seed.txt). BirthdayParadox() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) rng <- randomSource() console.showIntroduction() if count <- console.nextGroupSize() birthdays <- randomBirthdays(count, rng) console.showBirthdays(birthdays) if shared <- firstSharedBirthday(birthdays) console.showShared(shared) else console.showNoneShared() console.showSimulating(count) matchCount <- matchingGroups(count, rng) console.showResults(count, matchCount, sharedChance(matchCount))
How the port works
The original's main() asks, simulates, words and prints in the same lines. The port keeps the
three apart, as every port in this book does:
- The game runs the experiment.
randomBirthdays,hasSharedBirthday,firstSharedBirthday,matchingGroupsandsharedChancetake values and give back values; none of them reads input or prints. A birthday is a realDate— the start of the year 2001 plus a random number of days — somonth()andday()do the work a hand-written table of month lengths would. The month abbreviations are data, a list inmonthAbbreviation, as the book'sMONTHStuple is. hasSharedBirthdayis a single pure expression:uniqdrops repeated dates from a stream, so a group that shares a birthday comes out ofdistinctBirthdaysshorter than it went in, and the comparison is just a length check.firstSharedBirthdayis unset when every birthday in the group is different, and the program tests it with a guard,if shared <- firstSharedBirthday(birthdays), instead of comparing against a null-like sentinel.- The
Consoleasks and tells.nextGroupSize()asks until the game,isGroupSize(entry), accepts the answer, or the input runs out:while not rtn? and not input.ended(). A typedIntegerprogram argument would remove this validation entirely, but Birthday Paradox asks for its number interactively, as the original does.showBirthdayswords each date withsay.birthday(month, day)and joins them with one stream,cat labels | join with withComma. Sayholds the words — every sentence the player sees, one method each, with the numbers as parameters. "Apr 4" is a word too:birthday(month, day)decides the order, so a language that puts the day first needs only its own block.- The program is the flow, laid out as the original's
main(): ask, show the sample group and its match, then run the hundred thousand groups and report. It chooses the words withSay(language()) ?: Say("en"), so a language with no block of its own falls back to English.