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: