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 24 — Factor Finder

This is project 24 of the book, ported to EK9. Its chapter is still being written; until then, this page shows the port itself: a run of it, the whole program, and how it keeps to the original.

A sample run

The player's answers are piped in:

24
7
36
QUIT

Run it with:

ek9 factorfinder.ek9 < input.txt

It prints:

Factor Finder, by Al Sweigart al@inventwithpython.com

A number's factors are two numbers that, when multiplied with each
other, produce the number. For example, 2 x 13 = 26, so 2 and 13 are
factors of 26. 1 x 26 = 26, so 1 and 26 are also factors of 26. We
say that 26 has four factors: 1, 2, 13, and 26.

If a number only has two factors (1 and itself), we call that a prime
number. Otherwise, we call it a composite number.

Can you discover some prime numbers?

Enter a positive whole number to factor (or QUIT):
1, 2, 3, 4, 6, 8, 12, 24
Enter a positive whole number to factor (or QUIT):
1, 7
Enter a positive whole number to factor (or QUIT):
1, 2, 3, 4, 6, 9, 12, 18, 36
Enter a positive whole number to factor (or QUIT):

The program

#!ek9
defines module bigbook.factorfinder

  defines class

    //THE GAME's state: the divisors still to try for one number.

    //The divisors worth trying: 1, 2, ... while divisor * divisor <= number. Every factor above
    //the square root is number / divisor for one of these, so the search stops at the root.
    CandidateDivisors
      number as Integer?
      divisor as Integer: 1

      default private CandidateDivisors() as pure

      CandidateDivisors() as pure
        -> forNumber as Integer
        number :=? forNumber

      hasNext() as pure
        <- rtn as Boolean: divisor * divisor <= number

      //A copy: `divisor += 1` changes the Integer in place, so handing out `divisor` itself
      //would change the value the stream had already passed on.
      next()
        <- rtn as Integer: Integer(divisor)
        divisor += 1

      default operator ?

    //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 number to factor; unset when the player quits or the input ends.
      nextNumber()
        <- rtn as Integer: Integer()
        quitting <- false
        while not rtn? and not quitting and not input.ended()
          stdout.println(say.askNumber())
          if entry <- input.next()
            response <- entry.trim()
            if response.upperCase() == say.quit()
              quitting: true
            else
              rtn: numberToFactor(response)

      showFactors()
        -> factors as List of Integer
        listed <- cat factors | map with asText | join with withComma | collect as String
        stdout.println(listed)

      ended() as pure
        <- rtn as Boolean: input.ended()

      override operator ? as pure
        <- rtn as Boolean: true

  defines function

    //THE GAME: the arithmetic, with no input or output.

    //A positive whole number, or unset for anything else: Integer(text) is unset for anything
    //that is not a whole number, so this one guard does the original's isdecimal() check.
    numberToFactor() as pure
      -> entry as String
      <- rtn as Integer: Integer()
      if number <- Integer(entry) then number > 0
        rtn: number

    //The book's algorithm as one pipeline: each divisor up to the root gives a pair of factors,
    //and `sort | uniq` does the work of the original's set() and sort(). A perfect square's
    //root pairs with itself (6 x 6 = 36), and `uniq` keeps one of the two.
    factorsOf() as pure
      -> number as Integer
      <- rtn as List of Integer: List() of Integer
      divides <- (number) is Predicate of Integer as pure function
        r: number rem t == 0
      withPartner <- (number) is Function of (Integer, List of Integer) as pure function
        r: [t, number / t]
      rtn: cat CandidateDivisors(number)
        | filter by divides
        | map with withPartner
        | flatten
        | sort
        | uniq
        | collect as List of Integer

  defines function

    asText() as pure
      -> factor as Integer
      <- rtn as String: $factor

    withComma() as pure
      ->
        left as String
        right as String
      <- rtn as String: `${left}, ${right}`

    //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()
        `Factor Finder, by Al Sweigart al@inventwithpython.com

A number's factors are two numbers that, when multiplied with each
other, produce the number. For example, 2 x 13 = 26, so 2 and 13 are
factors of 26. 1 x 26 = 26, so 1 and 26 are also factors of 26. We
say that 26 has four factors: 1, 2, 13, and 26.

If a number only has two factors (1 and itself), we call that a prime
number. Otherwise, we call it a composite number.

Can you discover some prime numbers?
`

      askNumber()
        "Enter a positive whole number to factor (or QUIT):"

      quit()
        "QUIT"

  defines program

    //Factor Finder (Big Book project 24). Shows all the factors of each number typed, until QUIT.
    //The game decides; the Console asks and tells, in the player's words.
    FactorFinder()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))

      console.showIntroduction()
      running <- true
      while running and not console.ended()
        if number <- console.nextNumber()
          console.showFactors(factorsOf(number))
        else
          running: false

About this port