Project P005

Gradebook

Students and their scores in three subjects: report cards, a ranking where equal averages share a place, pass or fail, and the class average and median, all worked out exactly - from a text menu.

4 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-09-28

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

Keeps students and their scores in three subjects - math, english and science - and shows a report card for any student, a ranking, and a class summary: the class average, the median, how many pass, and each subject's average. A text menu; the scores live while the program runs.

  • main.eml - the menu loop and the questions asked, with their checks
  • grades.eml - the arithmetic: averages, the pass mark, the ranking and shared places, the class average, the median and the subject averages
  • report.eml - what the screen shows: the menu, the report card, the ranking and the summary
  • text.eml - trimming, lower case and reading a whole number

Everything is worked out exactly. An average of up to three scores is a whole number of sixths, so averages are compared, ranked and held against the pass mark (60) as whole numbers; they become tenths, rounded half up, only to be shown. Equal averages share a place and the next place is skipped (1, 1, 3), with equal averages listed in name order. The median of an even number of students is the mean of the two in the middle.

What is checked: a name is not empty, has at most 20 characters and is not already there in any case; a subject is one of the numbers offered; a score is a whole number from 0 to 100. Entering a score again replaces it and says what it was.

Sessions: sessions/basic.in adds four students, enters their scores (one student has no science score, one score is replaced), shows two report cards, the ranking - two students share first place - and the class summary; sessions/bad-input.in asks for the ranking and the summary with no scores, types an empty, a too-long and a repeated name, ids and subjects that are not there, and scores that are words, too high, negative, not whole or empty, then ranks a class where one student has no scores yet.

Built on the verified corpus cases exam-score-records (students' scores kept and averaged) and simple-quiz-grader (scores held against a pass mark).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal

== Gradebook: 0 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 9
Pick a number from 1 to 6.

== Gradebook: 0 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 4

-- ranking --
  No scores yet.

== Gradebook: 0 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 5

-- class summary --
  No scores yet.

== Gradebook: 0 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 1
name> 
A student needs a name.

== Gradebook: 0 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 1
name> Alexandra Montgomery-Smith
A name has at most 20 characters.

== Gradebook: 0 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 1
name> Ann
Added #1: Ann.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 1
name> ANN
There is already a student named Ann.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 7
No student with that id.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> x
No student with that id.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 1
1) math  2) english  3) science
subject> 4
Pick a subject from 1 to 3.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 1
1) math  2) english  3) science
subject> x
Pick a subject from 1 to 3.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 1
1) math  2) english  3) science
subject> 1
score> abc
A score is a whole number from 0 to 100.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 1
1) math  2) english  3) science
subject> 1
score> 101
A score is a whole number from 0 to 100.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 1
1) math  2) english  3) science
subject> 1
score> -5
A score is a whole number from 0 to 100.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 1
1) math  2) english  3) science
subject> 1
score> 7.5
A score is a whole number from 0 to 100.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 1
1) math  2) english  3) science
subject> 1
score> 
A score is a whole number from 0 to 100.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 3
student id> 1

-- report card: Ann --
  math         -
  english      -
  science      -
  (no scores yet)

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 4

-- ranking --
  No scores yet.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 1
1) math  2) english  3) science
subject> 1
score> 100
math for Ann: 100.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 1
name> Bo
Added #2: Bo.

== Gradebook: 2 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 4

-- ranking --
    1  Ann                  100.0  pass
    -  Bo                    no scores yet

== Gradebook: 2 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 6
Bye.
What was typed (52 lines)
9
4
5
1

1
Alexandra Montgomery-Smith
1
Ann
1
ANN
2
7
2
x
2
1
4
2
1
x
2
1
1
abc
2
1
1
101
2
1
1
-5
2
1
1
7.5
2
1
1

3
1
4
2
1
1
100
1
Bo
4
6

basic

interpreter: byte-equal

== Gradebook: 0 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 1
name> Ann
Added #1: Ann.

== Gradebook: 1 student ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 1
name> Ben
Added #2: Ben.

== Gradebook: 2 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 1
name> Cara
Added #3: Cara.

== Gradebook: 3 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 1
name> Dev
Added #4: Dev.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 1
1) math  2) english  3) science
subject> 1
score> 90
math for Ann: 90.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 1
1) math  2) english  3) science
subject> 2
score> 80
english for Ann: 80.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 1
1) math  2) english  3) science
subject> 3
score> 85
science for Ann: 85.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 2
1) math  2) english  3) science
subject> 1
score> 70
math for Ben: 70.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 2
1) math  2) english  3) science
subject> 2
score> 95
english for Ben: 95.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 2
1) math  2) english  3) science
subject> 3
score> 90
science for Ben: 90.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 3
1) math  2) english  3) science
subject> 1
score> 55
math for Cara: 55.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 3
1) math  2) english  3) science
subject> 2
score> 58
english for Cara: 58.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 3
1) math  2) english  3) science
subject> 2
score> 60
english for Cara: 60 (was 58).

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 4
1) math  2) english  3) science
subject> 1
score> 72
math for Dev: 72.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 4
1) math  2) english  3) science
subject> 2
score> 73
english for Dev: 73.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 2
student id> 4
1) math  2) english  3) science
subject> 3
score> 72
science for Dev: 72.

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 3
student id> 1

-- report card: Ann --
  math        90
  english     80
  science     85
  average   85.0  pass

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 3
student id> 3

-- report card: Cara --
  math        55  below 60
  english     60
  science      -
  average   57.5  FAIL

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 4

-- ranking --
    1  Ann                   85.0  pass
    1  Ben                   85.0  pass
    3  Dev                   72.3  pass
    4  Cara                  57.5  FAIL

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 5

-- class summary --
  students with scores  4 of 4
  class average         75.0
  median                78.7
  passing               3 of 4
  math average          71.8  (4 scores)
  english average       77.0  (4 scores)
  science average       82.3  (3 scores)

== Gradebook: 4 students ==
1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit
choice> 6
Bye.
What was typed (63 lines)
1
Ann
1
Ben
1
Cara
1
Dev
2
1
1
90
2
1
2
80
2
1
3
85
2
2
1
70
2
2
2
95
2
2
3
90
2
3
1
55
2
3
2
58
2
3
2
60
2
4
1
72
2
4
2
73
2
4
3
72
3
1
3
3
4
5
6

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

eml
# P005 gradebook: students, their scores in three subjects, report cards, a
# ranking and a class summary. The scores live while the program runs; the
# first twenty projects do not save to files.
import grades
import report
import text

def ask_student(students):
    # The student whose id is typed, or None once the problem has been shown.
    text.number(text.trim(input("student id> "))) => n
    for s in students:
        if s[0] == n:
            return s
    "No student with that id." ^0
    return None

def ask_subject():
    # The index of the subject picked, or -1 once the problem has been shown.
    "" => choices
    for k in [0:len(grades.subjects) - 1]:
        if choices != "":
            choices + "  " => choices
        choices + str(k + 1) + ") " + grades.subjects[k] => choices
    choices ^0
    text.number(text.trim(input("subject> "))) => k
    if k < 1 or k > len(grades.subjects):
        ("Pick a subject from 1 to " + str(len(grades.subjects)) + ".") ^0
        return -1
    return k - 1

def ask_score():
    # A whole score from 0 to 100, or -1 once the problem has been shown.
    text.number(text.trim(input("score> "))) => score
    if score < 0 or score > 100:
        "A score is a whole number from 0 to 100." ^0
        return -1
    return score

def name_problem(students, name):
    # What is wrong with name for a new student, or "" if nothing is.
    if name == "":
        return "A student needs a name."
    if len(name) > 20:
        return "A name has at most 20 characters."
    for s in students:
        if text.lower(s[1]) == text.lower(name):
            return "There is already a student named " + s[1] + "."
    return ""

[] => students
1 => next_id
True => running
while running:
    report.menu(students)
    text.trim(input("choice> ")) => choice
    if choice == "1":
        text.trim(input("name> ")) => name
        name_problem(students, name) => problem
        if problem != "":
            problem ^0
        else:
            students + [[next_id, name, [None for x in grades.subjects]]] => students
            ("Added #" + str(next_id) + ": " + name + ".") ^0
            next_id + 1 => next_id
    elif choice == "2":
        ask_student(students) => s
        if s != None:
            ask_subject() => k
            if k >= 0:
                ask_score() => score
                if score >= 0:
                    s[2][k] => old
                    score => s[2][k]
                    (grades.subjects[k] + " for " + s[1] + ": " + str(score)) => said
                    if old != None:
                        said + " (was " + str(old) + ")" => said
                    (said + ".") ^0
    elif choice == "3":
        ask_student(students) => s
        if s != None:
            report.card(s)
    elif choice == "4":
        report.ranking(students)
    elif choice == "5":
        report.summary(students)
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import grades
import report
import text

def ask_student(students):
    n = text.number(text.trim(input("student id> ")))
    for s in students:
        if s[0] == n:
            return s
    print("No student with that id.")
    return None

def ask_subject():
    choices = ""
    for k in range(0, len(grades.subjects)):
        if choices != "":
            choices = choices + "  "
        choices = choices + str(k + 1) + ") " + grades.subjects[k]
    print(choices)
    k = text.number(text.trim(input("subject> ")))
    if k < 1 or k > len(grades.subjects):
        print("Pick a subject from 1 to " + str(len(grades.subjects)) + ".")
        return -1
    return k - 1

def ask_score():
    score = text.number(text.trim(input("score> ")))
    if score < 0 or score > 100:
        print("A score is a whole number from 0 to 100.")
        return -1
    return score

def name_problem(students, name):
    if name == "":
        return "A student needs a name."
    if len(name) > 20:
        return "A name has at most 20 characters."
    for s in students:
        if text.lower(s[1]) == text.lower(name):
            return "There is already a student named " + s[1] + "."
    return ""

students = []
next_id = 1
running = True
while running:
    report.menu(students)
    choice = text.trim(input("choice> "))
    if choice == "1":
        name = text.trim(input("name> "))
        problem = name_problem(students, name)
        if problem != "":
            print(problem)
        else:
            students = students + [[next_id, name, [None for x in grades.subjects]]]
            print("Added #" + str(next_id) + ": " + name + ".")
            next_id = next_id + 1
    elif choice == "2":
        s = ask_student(students)
        if s != None:
            k = ask_subject()
            if k >= 0:
                score = ask_score()
                if score >= 0:
                    old = s[2][k]
                    s[2][k] = score
                    said = grades.subjects[k] + " for " + s[1] + ": " + str(score)
                    if old != None:
                        said = said + " (was " + str(old) + ")"
                    print(said + ".")
    elif choice == "3":
        s = ask_student(students)
        if s != None:
            report.card(s)
    elif choice == "4":
        report.ranking(students)
    elif choice == "5":
        report.summary(students)
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")

grades.eml

eml
# P005 gradebook - the arithmetic. A student is a list [id, name, scores]:
# scores has one place per subject, a whole number from 0 to 100 or None.
#
# Everything here is exact. An average of up to three scores is a whole
# number of SIXTHS (6 is the smallest number that 1, 2 and 3 all divide), so
# averages are compared, ranked and tested against the pass mark as whole
# numbers, and only turned into tenths, rounded half up, to be shown.
import text

["math", "english", "science"] => subjects
6 => sixths
60 => pass_mark

def div(a, b):
    # a // b for whole numbers, b > 0.
    return int((a - a % b) / b)

def entered(student):
    return [s for s in student[2] if s != None]

def average6(student):
    # The average score in sixths, or -1 when there is no score yet.
    entered(student) => scores
    if len(scores) == 0:
        return -1
    return div(sum(scores) * sixths, len(scores))

def passes(student):
    return average6(student) >= pass_mark * sixths

def tenths(v, d):
    # v / d in tenths, rounded half up (v >= 0, d > 0).
    return div(20 * v + d, 2 * d)

def shown(t):
    # Tenths as 85.0.
    return str(div(t, 10)) + "." + str(t % 10)

def ranked(students):
    # The students who have scores, highest average first; equal averages in
    # name order. Each goes after every student already placed who is ahead
    # of it or level with it.
    [] => order
    for s in students:
        average6(s) => a
        if a >= 0:
            0 => i
            while i < len(order) and (average6(order[i]) > a or (average6(order[i]) == a and text.lower(order[i][1]) <= text.lower(s[1]))):
                i + 1 => i
            order[0:i] + [s] + order[i:len(order)] => order
    return order

def place(order, student):
    # 1 + the number of students with a strictly higher average, so equal
    # averages share a place and the next place is skipped: 1, 1, 3.
    1 => p
    for s in order:
        if average6(s) > average6(student):
            p + 1 => p
    return p

def class_average(order):
    # The mean of the students' averages, in tenths.
    0 => total6
    for s in order:
        total6 + average6(s) => total6
    return tenths(total6, sixths * len(order))

def median(order):
    # The middle average in tenths; with an even number of students, the mean
    # of the two in the middle. order is highest first, which gives the same
    # middle as lowest first.
    len(order) => n
    div(n, 2) => m
    if n % 2 == 1:
        return tenths(average6(order[m]), sixths)
    return tenths(average6(order[m - 1]) + average6(order[m]), 2 * sixths)

def subject_average(students, k):
    # [average in tenths, number of scores] for subject k; [0, 0] if none.
    [s[2][k] for s in students if s[2][k] != None] => scores
    if len(scores) == 0:
        return [0, 0]
    return [tenths(sum(scores), len(scores)), len(scores)]
Python projection (grades.py)
import text
subjects = ["math", "english", "science"]
sixths = 6
pass_mark = 60

def div(a, b):
    return int((a - a % b) / b)

def entered(student):
    return [s for s in student[2] if s != None]

def average6(student):
    scores = entered(student)
    if len(scores) == 0:
        return -1
    return div(sum(scores) * sixths, len(scores))

def passes(student):
    return average6(student) >= pass_mark * sixths

def tenths(v, d):
    return div(20 * v + d, 2 * d)

def shown(t):
    return str(div(t, 10)) + "." + str(t % 10)

def ranked(students):
    order = []
    for s in students:
        a = average6(s)
        if a >= 0:
            i = 0
            while i < len(order) and (average6(order[i]) > a or average6(order[i]) == a and text.lower(order[i][1]) <= text.lower(s[1])):
                i = i + 1
            order = order[0:i] + [s] + order[i:len(order)]
    return order

def place(order, student):
    p = 1
    for s in order:
        if average6(s) > average6(student):
            p = p + 1
    return p

def class_average(order):
    total6 = 0
    for s in order:
        total6 = total6 + average6(s)
    return tenths(total6, sixths * len(order))

def median(order):
    n = len(order)
    m = div(n, 2)
    if n % 2 == 1:
        return tenths(average6(order[m]), sixths)
    return tenths(average6(order[m - 1]) + average6(order[m]), 2 * sixths)

def subject_average(students, k):
    scores = [s[2][k] for s in students if s[2][k] != None]
    if len(scores) == 0:
        return [0, 0]
    return [tenths(sum(scores), len(scores)), len(scores)]

report.eml

eml
# P005 gradebook - what the screen shows: the menu, a report card, the
# ranking and the class summary.
import grades

def plural(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def menu(students):
    "" ^0
    ("== Gradebook: " + plural(len(students), "student") + " ==") ^0
    "1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit" ^0

def verdict(student):
    if grades.passes(student):
        return "pass"
    return "FAIL"

def average_shown(student):
    return grades.shown(grades.tenths(grades.average6(student), grades.sixths))

def card(student):
    "" ^0
    ("-- report card: " + student[1] + " --") ^0
    for k in [0:len(grades.subjects) - 1]:
        student[2][k] => score
        ("  " + ("%-9s" % grades.subjects[k])) => line
        if score == None:
            line + "    -" => line
        else:
            line + ("%5d" % score) => line
            if score < grades.pass_mark:
                line + "  below " + str(grades.pass_mark) => line
        line ^0
    if grades.average6(student) < 0:
        "  (no scores yet)" ^0
    else:
        ("  " + ("%-9s" % "average") + ("%5s" % average_shown(student)) + "  " + verdict(student)) ^0

def ranking(students):
    grades.ranked(students) => order
    "" ^0
    "-- ranking --" ^0
    if len(order) == 0:
        "  No scores yet." ^0
        return None
    for s in order:
        ("  " + ("%3d" % grades.place(order, s)) + "  " + ("%-20s" % s[1]) + ("%6s" % average_shown(s)) + "  " + verdict(s)) ^0
    for s in students:
        if grades.average6(s) < 0:
            ("    -  " + ("%-20s" % s[1]) + "  no scores yet") ^0

def summary(students):
    grades.ranked(students) => order
    "" ^0
    "-- class summary --" ^0
    if len(order) == 0:
        "  No scores yet." ^0
        return None
    ("  students with scores  " + str(len(order)) + " of " + str(len(students))) ^0
    ("  class average         " + grades.shown(grades.class_average(order))) ^0
    ("  median                " + grades.shown(grades.median(order))) ^0
    ("  passing               " + str(len([s for s in order if grades.passes(s)])) + " of " + str(len(order))) ^0
    for k in [0:len(grades.subjects) - 1]:
        grades.subject_average(students, k) => sa
        ("  " + ("%-22s" % (grades.subjects[k] + " average"))) => line
        if sa[1] == 0:
            line + "-" => line
        else:
            line + grades.shown(sa[0]) + "  (" + plural(sa[1], "score") + ")" => line
        line ^0
Python projection (report.py)
import grades

def plural(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def menu(students):
    print("")
    print("== Gradebook: " + plural(len(students), "student") + " ==")
    print("1) add student  2) enter score  3) report card  4) ranking  5) class summary  6) quit")

def verdict(student):
    if grades.passes(student):
        return "pass"
    return "FAIL"

def average_shown(student):
    return grades.shown(grades.tenths(grades.average6(student), grades.sixths))

def card(student):
    print("")
    print("-- report card: " + student[1] + " --")
    for k in range(0, len(grades.subjects)):
        score = student[2][k]
        line = "  " + "%-9s" % grades.subjects[k]
        if score == None:
            line = line + "    -"
        else:
            line = line + "%5d" % score
            if score < grades.pass_mark:
                line = line + "  below " + str(grades.pass_mark)
        print(line)
    if grades.average6(student) < 0:
        print("  (no scores yet)")
    else:
        print("  " + "%-9s" % "average" + "%5s" % average_shown(student) + "  " + verdict(student))

def ranking(students):
    order = grades.ranked(students)
    print("")
    print("-- ranking --")
    if len(order) == 0:
        print("  No scores yet.")
        return None
    for s in order:
        print("  " + "%3d" % grades.place(order, s) + "  " + "%-20s" % s[1] + "%6s" % average_shown(s) + "  " + verdict(s))
    for s in students:
        if grades.average6(s) < 0:
            print("    -  " + "%-20s" % s[1] + "  no scores yet")

def summary(students):
    order = grades.ranked(students)
    print("")
    print("-- class summary --")
    if len(order) == 0:
        print("  No scores yet.")
        return None
    print("  students with scores  " + str(len(order)) + " of " + str(len(students)))
    print("  class average         " + grades.shown(grades.class_average(order)))
    print("  median                " + grades.shown(grades.median(order)))
    print("  passing               " + str(len([s for s in order if grades.passes(s)])) + " of " + str(len(order)))
    for k in range(0, len(grades.subjects)):
        sa = grades.subject_average(students, k)
        line = "  " + "%-22s" % (grades.subjects[k] + " average")
        if sa[1] == 0:
            line = line + "-"
        else:
            line = line + grades.shown(sa[0]) + "  (" + plural(sa[1], "score") + ")"
        print(line)

text.eml

eml
# P005 gradebook - text helpers. The interpreter that checks every session
# does not run string methods yet, so these are written out.

def trim(s):
    # s without the spaces at either end.
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def lower(s):
    # s with A-Z turned into a-z; everything else as it was.
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper_letters
    "abcdefghijklmnopqrstuvwxyz" => lower_letters
    "" => out
    for c in s:
        c => d
        for i in [0:25]:
            if upper_letters[i] == c:
                lower_letters[i] => d
        out + d => out
    return out

def number(s):
    # The value of s if it is digits only (at least one), otherwise -1.
    if s == "":
        return -1
    0 => n
    for c in s:
        if not (c in "0123456789"):
            return -1
        n * 10 + int(c) => n
    return n
Python projection (text.py)
def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def lower(s):
    upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lower_letters = "abcdefghijklmnopqrstuvwxyz"
    out = ""
    for c in s:
        d = c
        for i in range(0, 26):
            if upper_letters[i] == c:
                d = lower_letters[i]
        out = out + d
    return out

def number(s):
    if s == "":
        return -1
    n = 0
    for c in s:
        if not c in "0123456789":
            return -1
        n = n * 10 + int(c)
    return n

Built on these corpus cases