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 6 — Caesar Cipher
Caesar Cipher is the oldest trick in cryptography: shift every letter of a message along the alphabet by a fixed number of places, wrapping from Z back to A. The program asks whether to encrypt or decrypt, asks for a shift key between 0 and 25, then asks for the message — uppercased, since the cipher only touches letters A to Z — and prints the shifted result. Spaces and punctuation pass through untouched; decrypting with the same key that encrypted a message gets the original text back.
Adapted from project 6 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
Nothing here is random, so there is no seed. input.txt answers the three prompts in
order — encrypt, key 3, and the message:
e
3
Hello, World!
ek9 caesarcipher.ek9 < input.txt
None of those three answers is echoed back; only the program's own prompts and its final encrypted line appear below:
Caesar Cipher, by Al Sweigart al@inventwithpython.com
The Caesar cipher encrypts letters by shifting them over by a
key number. For example, a key of 2 means the letter A is
encrypted into C, the letter B encrypted into D, and so on.
Do you want to (e)ncrypt or (d)ecrypt?
Please enter the key (0 to 25) to use.
Enter the message to encrypt.
KHOOR, ZRUOG!
The program
#!ek9 defines module bigbook.caesarcipher defines constant Symbols <- "ABCDEFGHIJKLMNOPQRSTUVWXYZ" defines type CipherMode Encrypt Decrypt 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()) stdout.println("") //Asks until the answer is a mode; unset only if the input ends first. nextMode() <- rtn as CipherMode: CipherMode() while not rtn? and not input.ended() stdout.println(say.askMode()) rtn: modeOf(input.next()) if not rtn? and not input.ended() stdout.println(say.notAMode()) //Asks until the answer is a key; unset only if the input ends first. nextKey() <- rtn as Integer: Integer() while not rtn? and not input.ended() stdout.println(say.askKey(maxKey())) rtn: keyFrom(input.next()) //The message to translate; unset only if the input ends first. nextMessage() -> mode as CipherMode <- rtn as String: String() stdout.println(mode == CipherMode.Encrypt <- say.askMessageToEncrypt() : say.askMessageToDecrypt()) rtn: input.next() //The translation is computed, not worded: it is shown as the cipher made it. showTranslated() -> translated as String stdout.println(translated) override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: the cipher, with no input or output. //The first letter of the reply picks the mode; any other reply leaves it unset. modeOf() as pure -> reply as String <- rtn as CipherMode: CipherMode() initial <- reply.trim().lowerCase().first() if initial == 'e' rtn: CipherMode.Encrypt else if initial == 'd' rtn: CipherMode.Decrypt maxKey() as pure <- rtn as Integer: length Symbols - 1 //Integer(text) is unset when the text is not a number; a number outside 0 to maxKey() //is not a key either. keyFrom() as pure -> reply as String <- rtn as Integer: Integer() if candidate <- Integer(reply.trim()) then candidate >= 0 and candidate <= maxKey() rtn: candidate //A symbol's position comes from indexOf, which is unset for anything outside Symbols, //so the guard is also the "is this a letter?" test. mod is never negative, so one //expression wraps both directions. shiftedSymbol() as pure -> character as Character shift as Integer <- rtn as String: $character if position <- Symbols.indexOf(character) wrapped <- (position + shift) mod length Symbols rtn: Symbols.subString(wrapped, wrapped + 1) //The cipher works on capital letters only, so the message is uppercased first. translate() -> message as String mode as CipherMode key as Integer <- rtn as String: "" shift <- mode == CipherMode.Encrypt <- key : -key for character in message.upperCase() rtn += shiftedSymbol(character, shift) 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 player sees. defines text for "en" Say introduction() `Caesar Cipher, by Al Sweigart al@inventwithpython.com The Caesar cipher encrypts letters by shifting them over by a key number. For example, a key of 2 means the letter A is encrypted into C, the letter B encrypted into D, and so on.` askMode() "Do you want to (e)ncrypt or (d)ecrypt?" notAMode() "Please enter the letter e or d." askKey() -> highestKey as Integer `Please enter the key (0 to ${highestKey}) to use.` askMessageToEncrypt() "Enter the message to encrypt." askMessageToDecrypt() "Enter the message to decrypt." defines program //Caesar Cipher (Big Book project 6). Encrypts or decrypts a message with a chosen key. The //game decides; the Console asks and tells, in the player's words. CaesarCipher() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) console.showIntroduction() mode <- console.nextMode() key <- console.nextKey() if mode? and key? if message <- console.nextMessage(mode) console.showTranslated(translate(message, mode, key))
How the port works
The original's main() asks, checks, enciphers and prints in one run of lines. The port keeps
those apart, in the same shape as this book's other cipher ports (Simple Substitution,
project 66, and Vigenère, project 80), which have the same Console methods for the same
exchanges:
- The cipher decides.
modeOf,keyFrom,shiftedSymbolandtranslatetake values and give back values; none of them reads input or prints.CipherModeis a two-member enumeration (Encrypt,Decrypt).modeOfreads the first letter of the reply and sets the mode from it, leaving it unset for anything else;keyFromis unset for anything that is not a number from 0 to 25, with the range check in the guard itself:if candidate <- Integer(reply.trim()) then candidate >= 0 and candidate <= maxKey(). Symbols.indexOf(character)is unset for any character that is not one of the 26 letters, and that single guard does two jobs at once: it finds the letter's position in the alphabet, and it is also the test for "is this actually a letter?" — punctuation and spaces fail the guard and are returned unchanged byshiftedSymbol.- Wrapping the shifted position uses
mod, which in EK9 is never negative:(position + shift) mod length Symbolswraps correctly in both directions on its own. The original needs a separate adjustment for when the shifted number goes below zero; here that case simply doesn't arise. modeOf,keyFromandshiftedSymbolare allas pure.translateis not: it builds its output a character at a time withrtn +=in a loop, and apurefunction in EK9 is not allowed to mutate a local variable that way. It also uppercases the message, because that is a rule of this cipher — it only knows the capital letters.- The
Consoleasks and tells.nextMode()andnextKey()ask until the game accepts the reply or the input runs out —while not rtn? and not input.ended()— rather than awhile Trueguarded by abreak, so there is no separate flag to track whether a valid answer has been seen yet.nextMessage(mode)turns the mode into the right prompt,say.askMessageToEncrypt()orsay.askMessageToDecrypt(), andshowTranslatedprints the result as the cipher made it. Sayholds the words. The introduction is one method, a multi-line String holding the original's four lines; the blank line after it is layout, so the Console prints it. "Enter the message to encrypt." and "...to decrypt." are two sentences rather than one built from the mode's name, so each reads right and each could be worded freely in another language.- The program is the flow: ask for a mode and a key, and only once both were given ask
for the message and show its translation. It chooses the words with
Say(language()) ?: Say("en"), so a language with no words of its own falls back to English.