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 7 — Caesar Hacker
The Caesar cipher has only 26 possible keys, so breaking one needs no cleverness at all: try every key and read off the one result that comes out as English. Caesar Hacker does exactly that. Give it a shifted message and it prints all 26 decryptions, key 0 through key 25, so a human reader can spot the real one at a glance.
Adapted from project 7 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 message is a program argument (args.txt holds Khoor, Zruog!), so there is
nothing to type at a prompt:
ek9 caesar-hacker.ek9 "Khoor, Zruog!"
Key #0: Khoor, Zruog!
Key #1: Jgnnq, Yqtnf!
Key #2: Ifmmp, Xpsme!
Key #3: Hello, World!
Key #4: Gdkkn, Vnqkc!
Key #5: Fcjjm, Umpjb!
Key #6: Ebiil, Tloia!
Key #7: Dahhk, Sknhz!
Key #8: Czggj, Rjmgy!
Key #9: Byffi, Qilfx!
Key #10: Axeeh, Phkew!
Key #11: Zwddg, Ogjdv!
Key #12: Yvccf, Nficu!
Key #13: Xubbe, Mehbt!
Key #14: Wtaad, Ldgas!
Key #15: Vszzc, Kcfzr!
Key #16: Uryyb, Jbeyq!
Key #17: Tqxxa, Iadxp!
Key #18: Spwwz, Hzcwo!
Key #19: Rovvy, Gybvn!
Key #20: Qnuux, Fxaum!
Key #21: Pmttw, Ewztl!
Key #22: Olssv, Dvysk!
Key #23: Nkrru, Cuxrj!
Key #24: Mjqqt, Btwqi!
Key #25: Lipps, Asvph!
Key 3 is the one that reads as English: Hello, World!.
The program
#!ek9 defines module bigbook.caesarhacker defines constant Uppercase <- "ABCDEFGHIJKLMNOPQRSTUVWXYZ" Lowercase <- "abcdefghijklmnopqrstuvwxyz" AlphabetSize <- 26 defines class //THE OUTPUT: everything the reader sees goes through here, worded by Say. The message is a //program argument, so there is no LineInput. Console stdout as Stdout: Stdout() say as Say: Say("en") Console() -> words as Say say: words //The decryption is computed, not worded: Say only labels it with its key. showDecryption() -> key as Integer decryption as String stdout.println(say.decryptedWith(key, decryption)) override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: the cipher, with no output or words. //The shape of one step of the cipher, so a stream can map a message through it. CharacterMap() as pure abstract -> ch as Character <- rtn as Character? //EK9 has no ord/chr: a letter's position is its index in the alphabet, unset when it is //not in it - so the guard both finds the position and lets everything else pass through. shiftedWithin() as pure -> ch as Character alphabet as String shift as Integer <- rtn as Character: ch if position <- alphabet.indexOf(ch) //`mod` (unlike `rem`) is never negative, so a backward shift wraps round the alphabet. rtn: alphabet.subString((position + shift) mod AlphabetSize).first() shifted() as pure -> ch as Character shift as Integer <- rtn as Character: ch.isUpperCase() <- shiftedWithin(ch, Uppercase, shift) : shiftedWithin(ch, Lowercase, shift) //Decrypting with a key is shifting every letter back by it. decrypted() as pure -> message as String key as Integer <- rtn as String? back <- (key) is CharacterMap as pure function rtn: shifted(ch, -key) rtn: cat message | map with back | collect as String 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 decryptions are not here: the cipher makes them. defines text for "en" Say decryptedWith() -> key as Integer decryption as String `Key #${key}: ${decryption}` defines program //Caesar Hacker (Big Book project 7). Decrypts the message argument with every key, 0 to 25, //so the reader can see which one is English. The game decides; the Console shows, in the //reader's words. CaesarHacker() -> message as String //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) for key in 0 ... AlphabetSize - 1 console.showDecryption(key, decrypted(message, key))
How the port works
The original prints each try straight from its loop. The port keeps three things apart,
as every port in this book does: the cipher that decrypts, the Console that shows each
try, and Say, which holds the words. Here the words are a single line's label,
decryptedWith(key, decryption), "Key #3: Hello, World!": the decryption is the cipher's,
and the text only labels it. The message is a program argument, so the Console holds no
LineInput, only the Stdout and the Say, and has one method, showDecryption. The
program is the flow: one try per key, in words chosen with
Say(language()) ?: Say("en").
EK9 has no ord/chr, so a cipher cannot turn a letter into a number and back.
Instead, a letter's position in the alphabet string is found with
alphabet.indexOf(ch), which is unset for any character that is not in the
alphabet — punctuation and spaces included. That single guard,
if position <- alphabet.indexOf(ch), both finds the position for a real letter
and lets everything else fall through unchanged, testing for a position with ?
rather than comparing against a sentinel such as -1.
Shifting backwards to decrypt needs a wrap-around that never goes negative, and EK9
gives that for free: mod always returns a result with the same sign as its
divisor (-1 mod 26 is 25), while rem can come back negative (-1 rem 26 is
-1). Using mod for the wrap means no + 26 correction term is needed anywhere.
CharacterMap is declared as pure abstract — a function type, not a function —
and decrypted builds one for a particular key as a dynamic function that
captures that key: (key) is CharacterMap as pure function. The message is then
run through it with a stream, cat message | map with back | collect as String,
rather than an accumulator loop built up character by character.
The whole 26-key sweep is a single for key in 0 ... AlphabetSize - 1 loop with no
early exit, and every cipher function — shiftedWithin, shifted, decrypted — is
as pure and has exactly one declared result (<- rtn). The message arrives as a typed
String program argument rather than through an input() prompt, so there is no
validation loop to translate at all — the compiler has already guaranteed a String
arrived before CaesarHacker's body runs.