<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P019-poll/ | updated: 2026-10-03 -->

# P019 Poll

Set up a poll with two to six options, enter ranked ballots (one at a time or many identical ones at once), and count them four ways side by side - plurality, instant runoff round by round, Borda, and head to head - to see when the methods disagree, from a text menu.

EML-P project `projects/poll` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/poll` runs it and `eml project verify projects/poll` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: the-runoff-eliminated-the-one-everyone-could-accept (https://efficientnewlanguage.org/cases/878-the-runoff-eliminated-the-one-everyone-could-accept/), the-sincere-ballot-lost-to-the-strategic-one (https://efficientnewlanguage.org/cases/881-the-sincere-ballot-lost-to-the-strategic-one/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
7
2
3
4
1

1
A question that is far too long to fit in the sixty characters allowed
Is it fine?
Yes
yes

1
Tea or coffee?
Tea
Coffee
A name that is far too long
Water

3
2
D
A A
0 A
1001 B
x
3 a
c,b
c
2 b

4
3
5
```

Screen:

```text

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 7
Pick a number from 1 to 5.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 2
Set up a poll first.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 3
No ballots yet.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 4
No ballots yet.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 1
question> 
Cancelled.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 1
question> A question that is far too long to fit in the sixty characters allowed
At most 60 characters.
question> Is it fine?
Options, one per line - 2 to 6; an empty line ends the list.
option A> Yes
option B> yes
That option is already there.
option B> 
A poll needs at least two options; nothing changed.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 1
question> Tea or coffee?
Options, one per line - 2 to 6; an empty line ends the list.
option A> Tea
option B> Coffee
option C> A name that is far too long
At most 20 characters.
option C> Water
option D> 
Poll ready: Tea or coffee?
  A) Tea  B) Coffee  C) Water

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 3
No ballots yet.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 2
Rank by letter, most preferred first (like B A), or put a count first for that many identical ballots (like 40 A B). An empty line ends voting.
ballot> D
Type letters from A to C, each once, with a count from 1 to 1000 in front if you like.
ballot> A A
Type letters from A to C, each once, with a count from 1 to 1000 in front if you like.
ballot> 0 A
Type letters from A to C, each once, with a count from 1 to 1000 in front if you like.
ballot> 1001 B
Type letters from A to C, each once, with a count from 1 to 1000 in front if you like.
ballot> x
Type letters from A to C, each once, with a count from 1 to 1000 in front if you like.
ballot> 3 a
Counted 3 ballots: A.
ballot> c,b
Counted 1 ballot: C > B.
ballot> c
Counted 1 ballot: C.
ballot> 2 b
Counted 2 ballots: B.
ballot> 
7 ballots so far.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 4

-- ballots: 7 ballots, 4 different rankings --
     3  A
     1  C > B
     1  C
     2  B

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 3
Question: Tea or coffee?

-- results: 7 ballots --
Plurality - first choices only:
  A) Tea                      3   42.9%
  B) Coffee                   2   28.6%
  C) Water                    2   28.6%
  winner: Tea
Instant runoff - the last goes out and its ballots move to their next choice:
  round 1: Tea 3, Coffee 2, Water 2 - Water goes out
  round 2: Tea 3, Coffee 3 - Coffee goes out (1 ballot with no choice left)
  round 3: Tea 3 - Tea has more than half of 3 (4 ballots with no choice left)
  winner: Tea
Borda - 2 points for first place, one less for each place below:
  Tea 6, Coffee 5, Water 4
  winner: Tea
Head to head - every pair of options, one against the other:
  Tea and Coffee tie 3 to 3
  Tea beats Water 3 to 2
  Coffee and Water tie 2 to 2
  no option beats every other one
Winners: plurality Tea; instant runoff Tea; Borda Tea; head to head none.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 5
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
Where should the new library go?
Riverside
Hilltop
Downtown

2
40 A B C
35 C B A
25 B A C

3
4
5
```

Screen:

```text

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 1
question> Where should the new library go?
Options, one per line - 2 to 6; an empty line ends the list.
option A> Riverside
option B> Hilltop
option C> Downtown
option D> 
Poll ready: Where should the new library go?
  A) Riverside  B) Hilltop  C) Downtown

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 2
Rank by letter, most preferred first (like B A), or put a count first for that many identical ballots (like 40 A B). An empty line ends voting.
ballot> 40 A B C
Counted 40 ballots: A > B > C.
ballot> 35 C B A
Counted 35 ballots: C > B > A.
ballot> 25 B A C
Counted 25 ballots: B > A > C.
ballot> 
100 ballots so far.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 3
Question: Where should the new library go?

-- results: 100 ballots --
Plurality - first choices only:
  A) Riverside               40   40.0%
  C) Downtown                35   35.0%
  B) Hilltop                 25   25.0%
  winner: Riverside
Instant runoff - the last goes out and its ballots move to their next choice:
  round 1: Riverside 40, Downtown 35, Hilltop 25 - Hilltop goes out
  round 2: Riverside 65, Downtown 35 - Riverside has more than half of 100
  winner: Riverside
Borda - 2 points for first place, one less for each place below:
  Hilltop 125, Riverside 105, Downtown 70
  winner: Hilltop
Head to head - every pair of options, one against the other:
  Hilltop beats Riverside 60 to 40
  Riverside beats Downtown 65 to 35
  Hilltop beats Downtown 65 to 35
  winner: Hilltop, who beats every other option
Winners: plurality Riverside; instant runoff Riverside; Borda Hilltop; head to head Hilltop.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 4

-- ballots: 100 ballots, 3 different rankings --
    40  A > B > C
    35  C > B > A
    25  B > A > C

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 5
Bye.
```

### strategic - interpreter: byte-equal to the golden

Input:

```text
1
Which design should the club pick?
Alder
Birch
Cedar

2
55 A B C
45 B C A

3
1
Which design should the club pick?
Alder
Birch
Cedar

2
55 A C B
45 B C A

3
5
```

Screen:

```text

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 1
question> Which design should the club pick?
Options, one per line - 2 to 6; an empty line ends the list.
option A> Alder
option B> Birch
option C> Cedar
option D> 
Poll ready: Which design should the club pick?
  A) Alder  B) Birch  C) Cedar

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 2
Rank by letter, most preferred first (like B A), or put a count first for that many identical ballots (like 40 A B). An empty line ends voting.
ballot> 55 A B C
Counted 55 ballots: A > B > C.
ballot> 45 B C A
Counted 45 ballots: B > C > A.
ballot> 
100 ballots so far.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 3
Question: Which design should the club pick?

-- results: 100 ballots --
Plurality - first choices only:
  A) Alder                   55   55.0%
  B) Birch                   45   45.0%
  C) Cedar                    0    0.0%
  winner: Alder
Instant runoff - the last goes out and its ballots move to their next choice:
  round 1: Alder 55, Birch 45, Cedar 0 - Alder has more than half of 100
  winner: Alder
Borda - 2 points for first place, one less for each place below:
  Birch 145, Alder 110, Cedar 45
  winner: Birch
Head to head - every pair of options, one against the other:
  Alder beats Birch 55 to 45
  Alder beats Cedar 55 to 45
  Birch beats Cedar 100 to 0
  winner: Alder, who beats every other option
Winners: plurality Alder; instant runoff Alder; Borda Birch; head to head Alder.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 1
question> Which design should the club pick?
Options, one per line - 2 to 6; an empty line ends the list.
option A> Alder
option B> Birch
option C> Cedar
option D> 
Poll ready: Which design should the club pick?
  A) Alder  B) Birch  C) Cedar

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 2
Rank by letter, most preferred first (like B A), or put a count first for that many identical ballots (like 40 A B). An empty line ends voting.
ballot> 55 A C B
Counted 55 ballots: A > C > B.
ballot> 45 B C A
Counted 45 ballots: B > C > A.
ballot> 
100 ballots so far.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 3
Question: Which design should the club pick?

-- results: 100 ballots --
Plurality - first choices only:
  A) Alder                   55   55.0%
  B) Birch                   45   45.0%
  C) Cedar                    0    0.0%
  winner: Alder
Instant runoff - the last goes out and its ballots move to their next choice:
  round 1: Alder 55, Birch 45, Cedar 0 - Alder has more than half of 100
  winner: Alder
Borda - 2 points for first place, one less for each place below:
  Alder 110, Cedar 100, Birch 90
  winner: Alder
Head to head - every pair of options, one against the other:
  Alder beats Birch 55 to 45
  Alder beats Cedar 55 to 45
  Cedar beats Birch 55 to 45
  winner: Alder, who beats every other option
Winners: plurality Alder; instant runoff Alder; Borda Alder; head to head Alder.

== Poll ==
1) new poll  2) vote  3) results  4) ballots  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P019 poll: set up a poll, enter ranked ballots, and count them four ways
# side by side - plurality, instant runoff, Borda and head to head - so that
# when the methods disagree, the disagreement is on the screen.
import count
import text

"ABCDEF" => letters
6 => most_options
1000 => most_at_once

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

def names_text(options, items):
    # "Riverside", "Riverside and Downtown", "A, B and C".
    if len(items) == 1:
        return options[items[0]]
    "" => out
    for i in [0:len(items) - 2]:
        if i > 0:
            out + ", " => out
        out + options[items[i]] => out
    return out + " and " + options[items[len(items) - 1]]

def ranked(scores, k):
    # The options from highest score to lowest; equal scores in option order.
    [] => out
    for i in [0:k - 1]:
        len(out) => p
        out + [i] => out
        while p > 0 and scores[out[p - 1]] < scores[i]:
            out[p - 1] => out[p]
            p - 1 => p
        i => out[p]
    return out

def ask_text(prompt, longest):
    # Text of at most longest characters, asked again until it fits; "" ends.
    while True:
        text.trim(input(prompt)) => answer
        if len(answer) <= longest:
            return answer
        ("At most " + str(longest) + " characters.") ^0

def parse_ballot(answer, k):
    # [ranking, count] for a ballot like "B A C", "bac" or "40 B A C", or
    # None if it is not one.
    text.words(answer) => ws
    1 => n
    if len(ws) > 1 and text.number(ws[0]) >= 0:
        text.number(ws[0]) => n
        ws[1:len(ws)] => ws
    if n < 1 or n > most_at_once:
        return None
    "" => typed
    for w in ws:
        typed + text.upper(w) => typed
    if typed == "":
        return None
    [] => ranking
    for c in typed:
        0 - 1 => found
        for i in [0:k - 1]:
            if letters[i] == c:
                i => found
        if found == 0 - 1:
            return None
        for o in ranking:
            if o == found:
                return None
        ranking + [found] => ranking
    return [ranking, n]

def ranking_text(ranking):
    "" => out
    for p in [0:len(ranking) - 1]:
        if p > 0:
            out + " > " => out
        out + letters[ranking[p]] => out
    return out

def show_results(options, ballots):
    len(options) => k
    count.total(ballots) => n
    "" ^0
    ("-- results: " + plural(n, "ballot") + " --") ^0
    "Plurality - first choices only:" ^0
    count.plurality(ballots, k) => votes
    for i in ranked(votes, k):
        ("  " + letters[i] + ") " + ("%-20s" % options[i]) + ("%6d" % votes[i]) + ("%8s" % (text.tenths(100 * votes[i], n) + "%"))) ^0
    count.leaders(votes, k) => top
    if len(top) == 1:
        ("  winner: " + options[top[0]]) ^0
    else:
        ("  tie: " + names_text(options, top)) ^0
    [names_text(options, top)] => winners
    "Instant runoff - the last goes out and its ballots move to their next choice:" ^0
    count.runoff(ballots, k) => r
    for i in [0:len(r[0]) - 1]:
        r[0][i] => rd
        "" => line
        [] => still_in
        for o in ranked(rd[0], k):
            if not rd[3][o]:
                still_in + [o] => still_in
        for o in still_in:
            if line != "":
                line + ", " => line
            line + options[o] + " " + str(rd[0][o]) => line
        if rd[2] == 0 - 1:
            line + " - " + options[r[1]] + " has more than half of " + str(rd[1]) => line
        else:
            line + " - " + options[rd[2]] + " goes out" => line
        if rd[1] < n:
            line + " (" + plural(n - rd[1], "ballot") + " with no choice left)" => line
        ("  round " + str(i + 1) + ": " + line) ^0
    ("  winner: " + options[r[1]]) ^0
    winners + [options[r[1]]] => winners
    ("Borda - " + plural(k - 1, "point") + " for first place, one less for each place below:") ^0
    count.borda(ballots, k) => points
    "" => line
    for i in ranked(points, k):
        if line != "":
            line + ", " => line
        line + options[i] + " " + str(points[i]) => line
    ("  " + line) ^0
    count.leaders(points, k) => top
    if len(top) == 1:
        ("  winner: " + options[top[0]]) ^0
    else:
        ("  tie: " + names_text(options, top)) ^0
    winners + [names_text(options, top)] => winners
    "Head to head - every pair of options, one against the other:" ^0
    count.head_to_head(ballots, k) => wins
    for i in [0:k - 2]:
        for j in [i + 1:k - 1]:
            if wins[i][j] > wins[j][i]:
                ("  " + options[i] + " beats " + options[j] + " " + str(wins[i][j]) + " to " + str(wins[j][i])) ^0
            elif wins[j][i] > wins[i][j]:
                ("  " + options[j] + " beats " + options[i] + " " + str(wins[j][i]) + " to " + str(wins[i][j])) ^0
            else:
                ("  " + options[i] + " and " + options[j] + " tie " + str(wins[i][j]) + " to " + str(wins[j][i])) ^0
    count.beats_all(wins, k) => w
    if w == 0 - 1:
        "  no option beats every other one" ^0
        winners + ["none"] => winners
    else:
        ("  winner: " + options[w] + ", who beats every other option") ^0
        winners + [options[w]] => winners
    ("Winners: plurality " + winners[0] + "; instant runoff " + winners[1] + "; Borda " + winners[2] + "; head to head " + winners[3] + ".") ^0

"" => question
[] => options
[] => ballots
True => running
while running:
    "" ^0
    "== Poll ==" ^0
    "1) new poll  2) vote  3) results  4) ballots  5) quit" ^0
    text.trim(input("choice> ")) => choice
    if choice == "1":
        ask_text("question> ", 60) => q
        if q == "":
            "Cancelled." ^0
        else:
            ("Options, one per line - 2 to " + str(most_options) + "; an empty line ends the list.") ^0
            [] => new_options
            while len(new_options) < most_options:
                ask_text("option " + letters[len(new_options)] + "> ", 20) => name
                if name == "":
                    break
                False => clash
                for o in new_options:
                    if text.upper(o) == text.upper(name):
                        True => clash
                if clash:
                    "That option is already there." ^0
                else:
                    new_options + [name] => new_options
            if len(new_options) < 2:
                "A poll needs at least two options; nothing changed." ^0
            else:
                q => question
                new_options => options
                [] => ballots
                "" => listing
                for i in [0:len(options) - 1]:
                    listing + "  " + letters[i] + ") " + options[i] => listing
                ("Poll ready: " + question) ^0
                listing ^0
    elif choice == "2":
        if len(options) == 0:
            "Set up a poll first." ^0
        else:
            ("Rank by letter, most preferred first (like " + letters[1] + " " + letters[0] + "), or put a count first for that many identical ballots (like 40 " + letters[0] + " " + letters[1] + "). An empty line ends voting.") ^0
            while True:
                text.trim(input("ballot> ")) => answer
                if answer == "":
                    break
                parse_ballot(answer, len(options)) => b
                if b == None:
                    ("Type letters from A to " + letters[len(options) - 1] + ", each once, with a count from 1 to " + str(most_at_once) + " in front if you like.") ^0
                else:
                    ballots + [b] => ballots
                    ("Counted " + plural(b[1], "ballot") + ": " + ranking_text(b[0]) + ".") ^0
            (plural(count.total(ballots), "ballot") + " so far.") ^0
    elif choice == "3":
        if count.total(ballots) == 0:
            "No ballots yet." ^0
        else:
            ("Question: " + question) ^0
            show_results(options, ballots)
    elif choice == "4":
        if count.total(ballots) == 0:
            "No ballots yet." ^0
        else:
            [] => groups
            for b in ballots:
                False => merged
                for g in groups:
                    if not merged and ranking_text(g[0]) == ranking_text(b[0]):
                        g[1] + b[1] => g[1]
                        True => merged
                if not merged:
                    groups + [[b[0], b[1]]] => groups
            "" ^0
            ("-- ballots: " + plural(count.total(ballots), "ballot") + ", " + plural(len(groups), "different ranking") + " --") ^0
            for g in groups:
                (("%6d" % g[1]) + "  " + ranking_text(g[0])) ^0
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import count
import text
letters = "ABCDEF"
most_options = 6
most_at_once = 1000

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

def names_text(options, items):
    if len(items) == 1:
        return options[items[0]]
    out = ""
    for i in range(0, len(items) - 2+1):
        if i > 0:
            out = out + ", "
        out = out + options[items[i]]
    return out + " and " + options[items[len(items) - 1]]

def ranked(scores, k):
    out = []
    for i in range(0, k):
        p = len(out)
        out = out + [i]
        while p > 0 and scores[out[p - 1]] < scores[i]:
            out[p] = out[p - 1]
            p = p - 1
        out[p] = i
    return out

def ask_text(prompt, longest):
    while True:
        answer = text.trim(input(prompt))
        if len(answer) <= longest:
            return answer
        print("At most " + str(longest) + " characters.")

def parse_ballot(answer, k):
    ws = text.words(answer)
    n = 1
    if len(ws) > 1 and text.number(ws[0]) >= 0:
        n = text.number(ws[0])
        ws = ws[1:len(ws)]
    if n < 1 or n > most_at_once:
        return None
    typed = ""
    for w in ws:
        typed = typed + text.upper(w)
    if typed == "":
        return None
    ranking = []
    for c in typed:
        found = 0 - 1
        for i in range(0, k):
            if letters[i] == c:
                found = i
        if found == 0 - 1:
            return None
        for o in ranking:
            if o == found:
                return None
        ranking = ranking + [found]
    return [ranking, n]

def ranking_text(ranking):
    out = ""
    for p in range(0, len(ranking)):
        if p > 0:
            out = out + " > "
        out = out + letters[ranking[p]]
    return out

def show_results(options, ballots):
    k = len(options)
    n = count.total(ballots)
    print("")
    print("-- results: " + plural(n, "ballot") + " --")
    print("Plurality - first choices only:")
    votes = count.plurality(ballots, k)
    for i in ranked(votes, k):
        print("  " + letters[i] + ") " + "%-20s" % options[i] + "%6d" % votes[i] + "%8s" % (text.tenths(100 * votes[i], n) + "%"))
    top = count.leaders(votes, k)
    if len(top) == 1:
        print("  winner: " + options[top[0]])
    else:
        print("  tie: " + names_text(options, top))
    winners = [names_text(options, top)]
    print("Instant runoff - the last goes out and its ballots move to their next choice:")
    r = count.runoff(ballots, k)
    for i in range(0, len(r[0])):
        rd = r[0][i]
        line = ""
        still_in = []
        for o in ranked(rd[0], k):
            if not rd[3][o]:
                still_in = still_in + [o]
        for o in still_in:
            if line != "":
                line = line + ", "
            line = line + options[o] + " " + str(rd[0][o])
        if rd[2] == 0 - 1:
            line = line + " - " + options[r[1]] + " has more than half of " + str(rd[1])
        else:
            line = line + " - " + options[rd[2]] + " goes out"
        if rd[1] < n:
            line = line + " (" + plural(n - rd[1], "ballot") + " with no choice left)"
        print("  round " + str(i + 1) + ": " + line)
    print("  winner: " + options[r[1]])
    winners = winners + [options[r[1]]]
    print("Borda - " + plural(k - 1, "point") + " for first place, one less for each place below:")
    points = count.borda(ballots, k)
    line = ""
    for i in ranked(points, k):
        if line != "":
            line = line + ", "
        line = line + options[i] + " " + str(points[i])
    print("  " + line)
    top = count.leaders(points, k)
    if len(top) == 1:
        print("  winner: " + options[top[0]])
    else:
        print("  tie: " + names_text(options, top))
    winners = winners + [names_text(options, top)]
    print("Head to head - every pair of options, one against the other:")
    wins = count.head_to_head(ballots, k)
    for i in range(0, k - 2+1):
        for j in range(i + 1, k):
            if wins[i][j] > wins[j][i]:
                print("  " + options[i] + " beats " + options[j] + " " + str(wins[i][j]) + " to " + str(wins[j][i]))
            elif wins[j][i] > wins[i][j]:
                print("  " + options[j] + " beats " + options[i] + " " + str(wins[j][i]) + " to " + str(wins[i][j]))
            else:
                print("  " + options[i] + " and " + options[j] + " tie " + str(wins[i][j]) + " to " + str(wins[j][i]))
    w = count.beats_all(wins, k)
    if w == 0 - 1:
        print("  no option beats every other one")
        winners = winners + ["none"]
    else:
        print("  winner: " + options[w] + ", who beats every other option")
        winners = winners + [options[w]]
    print("Winners: plurality " + winners[0] + "; instant runoff " + winners[1] + "; Borda " + winners[2] + "; head to head " + winners[3] + ".")

question = ""
options = []
ballots = []
running = True
while running:
    print("")
    print("== Poll ==")
    print("1) new poll  2) vote  3) results  4) ballots  5) quit")
    choice = text.trim(input("choice> "))
    if choice == "1":
        q = ask_text("question> ", 60)
        if q == "":
            print("Cancelled.")
        else:
            print("Options, one per line - 2 to " + str(most_options) + "; an empty line ends the list.")
            new_options = []
            while len(new_options) < most_options:
                name = ask_text("option " + letters[len(new_options)] + "> ", 20)
                if name == "":
                    break
                clash = False
                for o in new_options:
                    if text.upper(o) == text.upper(name):
                        clash = True
                if clash:
                    print("That option is already there.")
                else:
                    new_options = new_options + [name]
            if len(new_options) < 2:
                print("A poll needs at least two options; nothing changed.")
            else:
                question = q
                options = new_options
                ballots = []
                listing = ""
                for i in range(0, len(options)):
                    listing = listing + "  " + letters[i] + ") " + options[i]
                print("Poll ready: " + question)
                print(listing)
    elif choice == "2":
        if len(options) == 0:
            print("Set up a poll first.")
        else:
            print("Rank by letter, most preferred first (like " + letters[1] + " " + letters[0] + "), or put a count first for that many identical ballots (like 40 " + letters[0] + " " + letters[1] + "). An empty line ends voting.")
            while True:
                answer = text.trim(input("ballot> "))
                if answer == "":
                    break
                b = parse_ballot(answer, len(options))
                if b == None:
                    print("Type letters from A to " + letters[len(options) - 1] + ", each once, with a count from 1 to " + str(most_at_once) + " in front if you like.")
                else:
                    ballots = ballots + [b]
                    print("Counted " + plural(b[1], "ballot") + ": " + ranking_text(b[0]) + ".")
            print(plural(count.total(ballots), "ballot") + " so far.")
    elif choice == "3":
        if count.total(ballots) == 0:
            print("No ballots yet.")
        else:
            print("Question: " + question)
            show_results(options, ballots)
    elif choice == "4":
        if count.total(ballots) == 0:
            print("No ballots yet.")
        else:
            groups = []
            for b in ballots:
                merged = False
                for g in groups:
                    if not merged and ranking_text(g[0]) == ranking_text(b[0]):
                        g[1] = g[1] + b[1]
                        merged = True
                if not merged:
                    groups = groups + [[b[0], b[1]]]
            print("")
            print("-- ballots: " + plural(count.total(ballots), "ballot") + ", " + plural(len(groups), "different ranking") + " --")
            for g in groups:
                print("%6d" % g[1] + "  " + ranking_text(g[0]))
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### count.eml

```eml
# P019 poll - four ways to count the same ranked ballots. A ballot group is
# [ranking, count]: the ranking lists option numbers, most preferred first,
# and need not rank every option; count is how many identical ballots it
# stands for. Every rule that could tie says how the tie is broken.

def total(ballots):
    0 => n
    for b in ballots:
        n + b[1] => n
    return n

def first_choices(ballots, k, out_of):
    # How many ballots put each of the k options first among those still in
    # (out_of[i] True means option i is out). A ballot with nobody left is
    # exhausted and counts for no one.
    [] => votes
    for i in [0:k - 1]:
        votes + [0] => votes
    for b in ballots:
        0 - 1 => top
        for o in b[0]:
            if top == 0 - 1 and not out_of[o]:
                o => top
        if top != 0 - 1:
            votes[top] + b[1] => votes[top]
    return votes

def plurality(ballots, k):
    [] => none_out
    for i in [0:k - 1]:
        none_out + [False] => none_out
    return first_choices(ballots, k, none_out)

def runoff(ballots, k):
    # Instant runoff: count first choices among the options still in; an
    # option with more than half of the ballots still counting wins;
    # otherwise the option with the fewest goes out and its ballots move to
    # their next choice. A tie for fewest is broken by the fewest first
    # choices in the first round, then by the option listed later. Returns
    # [rounds, winner], a round being [votes, counting, out, gone]: out is
    # the option that goes out (-1 in the round that has a winner) and gone[i]
    # says whether option i was already out when the round was counted.
    [] => out_of
    for i in [0:k - 1]:
        out_of + [False] => out_of
    plurality(ballots, k) => first_round
    [] => rounds
    while True:
        first_choices(ballots, k, out_of) => votes
        0 => counting
        0 => left
        0 - 1 => best
        for i in [0:k - 1]:
            if not out_of[i]:
                counting + votes[i] => counting
                left + 1 => left
                if best == 0 - 1 or votes[i] > votes[best]:
                    i => best
        if 2 * votes[best] > counting or left == 1:
            rounds + [[votes, counting, 0 - 1, [x for x in out_of]]] => rounds
            return [rounds, best]
        0 - 1 => worst
        for i in [0:k - 1]:
            if not out_of[i]:
                if worst == 0 - 1 or votes[i] < votes[worst]:
                    i => worst
                elif votes[i] == votes[worst] and first_round[i] <= first_round[worst]:
                    i => worst
        rounds + [[votes, counting, worst, [x for x in out_of]]] => rounds
        True => out_of[worst]

def borda(ballots, k):
    # Borda: with k options, first place earns k - 1 points, second k - 2,
    # and so on down to 0; an option a ballot leaves unranked earns 0.
    [] => points
    for i in [0:k - 1]:
        points + [0] => points
    for b in ballots:
        for p in [0:len(b[0]) - 1]:
            points[b[0][p]] + (k - 1 - p) * b[1] => points[b[0][p]]
    return points

def prefers(ranking, i, j):
    # Whether a ranking puts option i above option j (an unranked option is
    # below every ranked one).
    for o in ranking:
        if o == i:
            return True
        if o == j:
            return False
    return False

def head_to_head(ballots, k):
    # wins[i][j]: how many ballots prefer option i to option j.
    [] => wins
    for i in [0:k - 1]:
        [] => row
        for j in [0:k - 1]:
            0 => n
            if i != j:
                for b in ballots:
                    if prefers(b[0], i, j):
                        n + b[1] => n
            row + [n] => row
        wins + [row] => wins
    return wins

def beats_all(wins, k):
    # The option that beats every other head to head, or -1 if there is none.
    for i in [0:k - 1]:
        True => all_beaten
        for j in [0:k - 1]:
            if i != j and wins[i][j] <= wins[j][i]:
                False => all_beaten
        if all_beaten:
            return i
    return 0 - 1

def leaders(scores, k):
    # The options with the highest score, in option order.
    0 => best
    for i in [0:k - 1]:
        if scores[i] > best:
            scores[i] => best
    [] => out
    for i in [0:k - 1]:
        if scores[i] == best:
            out + [i] => out
    return out
```

Python projection of count.eml:

```python
def total(ballots):
    n = 0
    for b in ballots:
        n = n + b[1]
    return n

def first_choices(ballots, k, out_of):
    votes = []
    for i in range(0, k):
        votes = votes + [0]
    for b in ballots:
        top = 0 - 1
        for o in b[0]:
            if top == 0 - 1 and not out_of[o]:
                top = o
        if top != 0 - 1:
            votes[top] = votes[top] + b[1]
    return votes

def plurality(ballots, k):
    none_out = []
    for i in range(0, k):
        none_out = none_out + [False]
    return first_choices(ballots, k, none_out)

def runoff(ballots, k):
    out_of = []
    for i in range(0, k):
        out_of = out_of + [False]
    first_round = plurality(ballots, k)
    rounds = []
    while True:
        votes = first_choices(ballots, k, out_of)
        counting = 0
        left = 0
        best = 0 - 1
        for i in range(0, k):
            if not out_of[i]:
                counting = counting + votes[i]
                left = left + 1
                if best == 0 - 1 or votes[i] > votes[best]:
                    best = i
        if 2 * votes[best] > counting or left == 1:
            rounds = rounds + [[votes, counting, 0 - 1, [x for x in out_of]]]
            return [rounds, best]
        worst = 0 - 1
        for i in range(0, k):
            if not out_of[i]:
                if worst == 0 - 1 or votes[i] < votes[worst]:
                    worst = i
                elif votes[i] == votes[worst] and first_round[i] <= first_round[worst]:
                    worst = i
        rounds = rounds + [[votes, counting, worst, [x for x in out_of]]]
        out_of[worst] = True

def borda(ballots, k):
    points = []
    for i in range(0, k):
        points = points + [0]
    for b in ballots:
        for p in range(0, len(b[0])):
            points[b[0][p]] = points[b[0][p]] + (k - 1 - p) * b[1]
    return points

def prefers(ranking, i, j):
    for o in ranking:
        if o == i:
            return True
        if o == j:
            return False
    return False

def head_to_head(ballots, k):
    wins = []
    for i in range(0, k):
        row = []
        for j in range(0, k):
            n = 0
            if i != j:
                for b in ballots:
                    if prefers(b[0], i, j):
                        n = n + b[1]
            row = row + [n]
        wins = wins + [row]
    return wins

def beats_all(wins, k):
    for i in range(0, k):
        all_beaten = True
        for j in range(0, k):
            if i != j and wins[i][j] <= wins[j][i]:
                all_beaten = False
        if all_beaten:
            return i
    return 0 - 1

def leaders(scores, k):
    best = 0
    for i in range(0, k):
        if scores[i] > best:
            best = scores[i]
    out = []
    for i in range(0, k):
        if scores[i] == best:
            out = out + [i]
    return out
```

### text.eml

```eml
# P019 poll - reading what is typed. The interpreter that checks every
# session does not run string methods yet, so the text handling is written
# out here.

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 upper(s):
    # s with a-z turned into A-Z; everything else as it was.
    "abcdefghijklmnopqrstuvwxyz" => lower_letters
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper_letters
    "" => out
    for c in s:
        c => d
        for i in [0:25]:
            if lower_letters[i] == c:
                upper_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 0 - 1
    0 => n
    for c in s:
        if not (c in "0123456789"):
            return 0 - 1
        n * 10 + int(c) => n
    return n

def words(s):
    # The words of s, split at spaces and commas.
    [] => out
    "" => word
    for c in s:
        if c == " " or c == ",":
            if word != "":
                out + [word] => out
            "" => word
        else:
            word + c => word
    if word != "":
        out + [word] => out
    return out

def tenths(n, d):
    # n / d to one decimal, rounded half up, as text: 2 / 3 is "0.7".
    2 * 10 * n + d => t
    int((t - t % (2 * d)) / (2 * d)) => v
    return str(int((v - v % 10) / 10)) + "." + str(v % 10)
```

Python projection of text.eml:

```python
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 upper(s):
    lower_letters = "abcdefghijklmnopqrstuvwxyz"
    upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    out = ""
    for c in s:
        d = c
        for i in range(0, 26):
            if lower_letters[i] == c:
                d = upper_letters[i]
        out = out + d
    return out

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

def words(s):
    out = []
    word = ""
    for c in s:
        if c == " " or c == ",":
            if word != "":
                out = out + [word]
            word = ""
        else:
            word = word + c
    if word != "":
        out = out + [word]
    return out

def tenths(n, d):
    t = 2 * 10 * n + d
    v = int((t - t % (2 * d)) / (2 * d))
    return str(int((v - v % 10) / 10)) + "." + str(v % 10)
```

## README

# P019 - Poll

Set up a poll - a question and two to six options, lettered A to F - enter
ranked ballots, and count them four ways side by side: plurality, instant
runoff (round by round), Borda, and head to head. The four methods often
agree; the program is built to show it plainly when they do not. A text
menu; the poll lives while the program runs.

- `main.eml` - the menu, setting up a poll, reading ballots, and the results
  and ballot screens
- `count.eml` - the four counts, the instant-runoff rounds, and the
  head-to-head winner
- `text.eml` - trimming, capitals, whole numbers, words and percentages

A ballot ranks options by letter, most preferred first ("B A C", "bac" or
"c,b"), and need not rank them all. A count in front ("40 A B C") enters that
many identical ballots - the way paper ballots are tallied.

The four counts:

- Plurality: only first choices count; the most wins.
- Instant runoff: count first choices among the options still in; an option
  with more than half of the ballots still counting wins; otherwise the
  option with the fewest goes out and its ballots move to their next choice.
  A ballot with no choice left is exhausted and counts for no one. A tie for
  fewest is broken by the fewest first choices in the first round, then by
  the option listed later.
- Borda: with k options, first place earns k - 1 points, second k - 2, and so
  on; an option a ballot leaves unranked earns nothing.
- Head to head: every pair of options, counting the ballots that put one
  above the other (an unranked option is below every ranked one). The winner
  is the option that beats every other; there may be none.

What is checked: a question has at most 60 characters and an option at most
20, options are not repeated in any case, and a poll needs at least two (a
failed setup leaves the old poll as it was); a ballot uses only the poll's
letters, each once, with a count from 1 to 1000 if any.

Sessions: `sessions/basic.in` has 100 voters choose a library site 40/35/25,
where plurality and instant runoff pick Riverside while Borda and head to
head pick Hilltop, the option nobody puts last; `sessions/strategic.in`
counts a club vote twice - sincere, Borda elects the second choice of the
55-voter majority, and when those voters rank it last instead, Borda elects
their own first choice; `sessions/bad-input.in` types questions, options and
ballots that are empty, too long, repeated, outside the letters or with a
count out of range, then counts a poll whose instant runoff needs both
tie-breaks and exhausts ballots, and where no option beats every other.

Built on the verified corpus cases
`the-runoff-eliminated-the-one-everyone-could-accept` (instant runoff
eliminating the option that would beat each other one head to head) and
`the-sincere-ballot-lost-to-the-strategic-one` (a Borda count won by burying a
real second choice).
