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 53 — Periodic Table of the Elements

This is project 53 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:

30

As

Quit

Run it with:

ek9 periodic-table-of-the-elements.ek9 < input.txt

It prints:

Periodic Table of Elements
By Al Sweigart al@inventwithpython.com

            Periodic Table of Elements
      1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18
    1 H                                                  He
    2 Li Be                               B  C  N  O  F  Ne
    3 Na Mg                               Al Si P  S  Cl Ar
    4 K  Ca Sc Ti V  Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr
    5 Rb Sr Y  Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I  Xe
    6 Cs Ba La Hf Ta W  Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn
    7 Fr Ra Ac Rf Db Sg Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og

            Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu
            Th Pa U  Np Pu Am Cm Bk Cf Es Fm Md No Lr
Enter a symbol or atomic number to examine, or QUIT to quit.
             Atomic Number: 30
                    Symbol: Zn
                   Element: Zinc
            Origin of name: Most likely from German Zinke, 'prong' or 'tooth', though some suggest Persian sang, 'stone'
                     Group: 12
                    Period: 4
             Atomic weight: 65.38(2) u
                   Density: 7.134 g/cm^3
             Melting point: 692.88 K
             Boiling point: 1180 K
    Specific heat capacity: 0.388 J/(g*K)
         Electronegativity: 1.65
Abundance in earth's crust: 70 mg/kg
Press Enter to continue...
            Periodic Table of Elements
      1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18
    1 H                                                  He
    2 Li Be                               B  C  N  O  F  Ne
    3 Na Mg                               Al Si P  S  Cl Ar
    4 K  Ca Sc Ti V  Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr
    5 Rb Sr Y  Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I  Xe
    6 Cs Ba La Hf Ta W  Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn
    7 Fr Ra Ac Rf Db Sg Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og

            Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu
            Th Pa U  Np Pu Am Cm Bk Cf Es Fm Md No Lr
Enter a symbol or atomic number to examine, or QUIT to quit.
             Atomic Number: 33
                    Symbol: As
                   Element: Arsenic
            Origin of name: French arsenic, from Greek arsenikón 'yellow arsenic' (influenced by arsenikós, 'masculine' or 'virile'), from a West Asian wanderword ultimately from Old Iranian *zarniya-ka, 'golden'
                     Group: 15
                    Period: 4
             Atomic weight: 74.921595(6) u
                   Density: 5.776 g/cm^3
             Melting point: 1090  K
             Boiling point: 887 K
    Specific heat capacity: 0.329 J/(g*K)
         Electronegativity: 2.18
Abundance in earth's crust: 1.8 mg/kg
Press Enter to continue...
            Periodic Table of Elements
      1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18
    1 H                                                  He
    2 Li Be                               B  C  N  O  F  Ne
    3 Na Mg                               Al Si P  S  Cl Ar
    4 K  Ca Sc Ti V  Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr
    5 Rb Sr Y  Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I  Xe
    6 Cs Ba La Hf Ta W  Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn
    7 Fr Ra Ac Rf Db Sg Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og

            Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu
            Th Pa U  Np Pu Am Cm Bk Cf Es Fm Md No Lr
Enter a symbol or atomic number to examine, or QUIT to quit.

The program

#!ek9
defines module bigbook.periodictable

  defines constant

    //Wikipedia's footnote marks, such as the [III][IV] in Boron's "10.81[III][IV][V][VI]".
    FootnoteMark <- /\[(I|V|X)+\]/

  defines type

    List of Detail

  defines record

    //One row of periodictable.csv, cleaned. Each column has a name, so nothing depends on a
    //value and its label sitting at the same position in two lists.
    Element
      number as String: String()
      symbol as String: String()
      name as String: String()
      origin as String: String()
      group as String: String()
      period as String: String()
      weight as String: String()
      density as String: String()
      meltingPoint as String: String()
      boilingPoint as String: String()
      specificHeat as String: String()
      electronegativity as String: String()
      abundance as String: String()

      //Pure, so that finding an element (and the unset one for no element) is pure too.
      default Element() as pure

      default operator ?

    //One line of an element's details: its label, and what is shown beside it.
    Detail
      label as String: String()
      reading as String: String()

      Detail()
        ->
          label as String
          reading as String

        this.label: label
        this.reading: reading

      default operator ?

  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())

      showTable()
        stdout.println(say.table())

      //What the player typed; unset when they QUIT or the input ends.
      nextEntry()
        <- rtn as String: String()
        stdout.println(say.askElement())
        if entry <- input.next()
          if entry.trim().upperCase() <> say.quit()
            rtn: entry

      //One line per field, in the book's order, each label right-aligned to the longest.
      showElement()
        -> element as Element
        details <- detailsOf(element)
        width <- labelWidth(details)
        for detail in details
          stdout.println(say.detail(detail.label.leftPadded(width), detail.reading))

      //The book waits for Enter before it shows the table again; what is typed is not used.
      waitForEnter()
        stdout.println(say.pressEnter())
        input.next()

      //Each field with its label and, where it has one, its unit.
      private detailsOf()
        -> element as Element
        <- rtn as List of Detail: [
          Detail(say.atomicNumber(), element.number),
          Detail(say.symbol(), element.symbol),
          Detail(say.element(), element.name),
          Detail(say.originOfName(), element.origin),
          Detail(say.group(), element.group),
          Detail(say.period(), element.period),
          Detail(say.atomicWeight(), say.inAtomicMassUnits(element.weight)),
          Detail(say.density(), say.inGramsPerCubicCentimetre(element.density)),
          Detail(say.meltingPoint(), say.inKelvin(element.meltingPoint)),
          Detail(say.boilingPoint(), say.inKelvin(element.boilingPoint)),
          Detail(say.specificHeat(), say.inJoulesPerGramKelvin(element.specificHeat)),
          Detail(say.electronegativity(), element.electronegativity),
          Detail(say.abundance(), say.inMilligramsPerKilogram(element.abundance))
        ]

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

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

  defines function

    //THE GAME: finding an element, with no input or output.

    //title() makes "as", "AS" and "As" the same symbol, as the book's .title() does. Unset when
    //the table has no such symbol or atomic number.
    elementFor() as pure
      ->
        elements as Dict of (String, Element)
        entry as String
      <- rtn as Element: elements.getOrDefault(entry.trim().title(), Element())

  defines function

    //THE DATA: the table read from its file, which the program asks for once.

    //A column without its footnote marks: the original's re.sub.
    column() as pure
      ->
        row as CSVRow
        index as Integer
      <- rtn as String?

      text <- row.getOrDefault(index, "")
      rtn: text.replace(FootnoteMark, "")

    elementOf()
      -> row as CSVRow
      <- rtn as Element: Element()
      rtn.number: column(row, 0)
      rtn.symbol: column(row, 1)
      rtn.name: column(row, 2)
      rtn.origin: column(row, 3)
      rtn.group: column(row, 4)
      rtn.period: column(row, 5)
      rtn.weight: column(row, 6)
      rtn.density: column(row, 7)
      rtn.meltingPoint: column(row, 8)
      rtn.boilingPoint: column(row, 9)
      rtn.specificHeat: column(row, 10)
      rtn.electronegativity: column(row, 11)
      rtn.abundance: column(row, 12)

    onNewLine() as pure
      ->
        above as String
        below as String
      <- rtn as String: `${above}\n${below}`

    //Every element in the file, findable by atomic number and by symbol.
    loadElements()
      -> path as FileSystemPath
      <- rtn as Dict of (String, Element): Dict() of (String, Element)
      text <- String()
      try
        -> input <- TextFile(path).input()
        text: cat input | join with onNewLine | collect as String
      result <- CSV(',', false).parse(text)
      if result?
        for row in result.ok()
          element <- elementOf(row)
          rtn += DictEntry(element.number, element)
          rtn += DictEntry(element.symbol, element)

  defines function

    //The longest label, which every label is right-aligned to (the book's LONGEST_COLUMN).
    labelWidth() as pure
      -> details as List of Detail
      <- rtn as Integer: 0
      for detail in details
        labelLength <- length detail.label
        rtn: rtn >? labelLength

    //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()
        `Periodic Table of Elements
By Al Sweigart al@inventwithpython.com
`

      //The book's picture of the table, printed as it is.
      table()
        `            Periodic Table of Elements
      1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18
    1 H                                                  He
    2 Li Be                               B  C  N  O  F  Ne
    3 Na Mg                               Al Si P  S  Cl Ar
    4 K  Ca Sc Ti V  Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr
    5 Rb Sr Y  Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I  Xe
    6 Cs Ba La Hf Ta W  Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn
    7 Fr Ra Ac Rf Db Sg Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og

            Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu
            Th Pa U  Np Pu Am Cm Bk Cf Es Fm Md No Lr`

      askElement()
        "Enter a symbol or atomic number to examine, or QUIT to quit."

      quit()
        "QUIT"

      atomicNumber()
        "Atomic Number"

      symbol()
        "Symbol"

      element()
        "Element"

      originOfName()
        "Origin of name"

      group()
        "Group"

      period()
        "Period"

      atomicWeight()
        "Atomic weight"

      density()
        "Density"

      meltingPoint()
        "Melting point"

      boilingPoint()
        "Boiling point"

      specificHeat()
        "Specific heat capacity"

      electronegativity()
        "Electronegativity"

      abundance()
        "Abundance in earth's crust"

      inAtomicMassUnits()
        -> weight as String
        `${weight} u`

      inGramsPerCubicCentimetre()
        -> density as String
        `${density} g/cm^3`

      inKelvin()
        -> temperature as String
        `${temperature} K`

      inJoulesPerGramKelvin()
        -> heat as String
        `${heat} J/(g*K)`

      inMilligramsPerKilogram()
        -> abundance as String
        `${abundance} mg/kg`

      detail()
        ->
          label as String
          reading as String
        `${label}: ${reading}`

      pressEnter()
        "Press Enter to continue..."

  defines program

    //Periodic Table of the Elements (Big Book project 53). Look up an element by its symbol or
    //atomic number and see what is known about it. The game decides; the Console asks and tells,
    //in the player's words.
    PeriodicTable()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      path <- FileSystemPath().inCurrentWorkingDirectory("data/periodictable.csv")
      if path.exists()
        elements <- loadElements(path)

        console.showIntroduction()
        running <- true
        while running and not console.ended()
          console.showTable()
          if entry <- console.nextEntry()
            if element <- elementFor(elements, entry)
              console.showElement(element)
              console.waitForEnter()
          else
            running: false

About this port