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.
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
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 screenscount.eml- the four counts, the instant-runoff rounds, and the head-to-head winnertext.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).
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
== 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.
What was typed (34 lines)
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
basic
interpreter: byte-equal
== 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.
What was typed (14 lines)
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
strategic
interpreter: byte-equal
== 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.
What was typed (23 lines)
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
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# 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 (main.py)
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 (count.py)
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 (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 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)