{
  "id": "P019",
  "slug": "poll",
  "key": "P019-poll",
  "title": "Poll",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P019 - Poll\n\nSet up a poll - a question and two to six options, lettered A to F - enter\nranked ballots, and count them four ways side by side: plurality, instant\nrunoff (round by round), Borda, and head to head. The four methods often\nagree; the program is built to show it plainly when they do not. A text\nmenu; the poll lives while the program runs.\n\n- `main.eml` - the menu, setting up a poll, reading ballots, and the results\n  and ballot screens\n- `count.eml` - the four counts, the instant-runoff rounds, and the\n  head-to-head winner\n- `text.eml` - trimming, capitals, whole numbers, words and percentages\n\nA ballot ranks options by letter, most preferred first (\"B A C\", \"bac\" or\n\"c,b\"), and need not rank them all. A count in front (\"40 A B C\") enters that\nmany identical ballots - the way paper ballots are tallied.\n\nThe four counts:\n\n- Plurality: only first choices count; the most wins.\n- Instant runoff: count first choices among the options still in; an option\n  with more than half of the ballots still counting wins; otherwise the\n  option with the fewest goes out and its ballots move to their next choice.\n  A ballot with no choice left is exhausted and counts for no one. A tie for\n  fewest is broken by the fewest first choices in the first round, then by\n  the option listed later.\n- Borda: with k options, first place earns k - 1 points, second k - 2, and so\n  on; an option a ballot leaves unranked earns nothing.\n- Head to head: every pair of options, counting the ballots that put one\n  above the other (an unranked option is below every ranked one). The winner\n  is the option that beats every other; there may be none.\n\nWhat is checked: a question has at most 60 characters and an option at most\n20, options are not repeated in any case, and a poll needs at least two (a\nfailed setup leaves the old poll as it was); a ballot uses only the poll's\nletters, each once, with a count from 1 to 1000 if any.\n\nSessions: `sessions/basic.in` has 100 voters choose a library site 40/35/25,\nwhere plurality and instant runoff pick Riverside while Borda and head to\nhead pick Hilltop, the option nobody puts last; `sessions/strategic.in`\ncounts a club vote twice - sincere, Borda elects the second choice of the\n55-voter majority, and when those voters rank it last instead, Borda elects\ntheir own first choice; `sessions/bad-input.in` types questions, options and\nballots that are empty, too long, repeated, outside the letters or with a\ncount out of range, then counts a poll whose instant runoff needs both\ntie-breaks and exhausts ballots, and where no option beats every other.\n\nBuilt on the verified corpus cases\n`the-runoff-eliminated-the-one-everyone-could-accept` (instant runoff\neliminating the option that would beat each other one head to head) and\n`the-sincere-ballot-lost-to-the-strategic-one` (a Borda count won by burying a\nreal second choice).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P019 poll: set up a poll, enter ranked ballots, and count them four ways\n# side by side - plurality, instant runoff, Borda and head to head - so that\n# when the methods disagree, the disagreement is on the screen.\nimport count\nimport text\n\n\"ABCDEF\" => letters\n6 => most_options\n1000 => most_at_once\n\ndef plural(n, word):\n    if n == 1:\n        return \"1 \" + word\n    return str(n) + \" \" + word + \"s\"\n\ndef names_text(options, items):\n    # \"Riverside\", \"Riverside and Downtown\", \"A, B and C\".\n    if len(items) == 1:\n        return options[items[0]]\n    \"\" => out\n    for i in [0:len(items) - 2]:\n        if i > 0:\n            out + \", \" => out\n        out + options[items[i]] => out\n    return out + \" and \" + options[items[len(items) - 1]]\n\ndef ranked(scores, k):\n    # The options from highest score to lowest; equal scores in option order.\n    [] => out\n    for i in [0:k - 1]:\n        len(out) => p\n        out + [i] => out\n        while p > 0 and scores[out[p - 1]] < scores[i]:\n            out[p - 1] => out[p]\n            p - 1 => p\n        i => out[p]\n    return out\n\ndef ask_text(prompt, longest):\n    # Text of at most longest characters, asked again until it fits; \"\" ends.\n    while True:\n        text.trim(input(prompt)) => answer\n        if len(answer) <= longest:\n            return answer\n        (\"At most \" + str(longest) + \" characters.\") ^0\n\ndef parse_ballot(answer, k):\n    # [ranking, count] for a ballot like \"B A C\", \"bac\" or \"40 B A C\", or\n    # None if it is not one.\n    text.words(answer) => ws\n    1 => n\n    if len(ws) > 1 and text.number(ws[0]) >= 0:\n        text.number(ws[0]) => n\n        ws[1:len(ws)] => ws\n    if n < 1 or n > most_at_once:\n        return None\n    \"\" => typed\n    for w in ws:\n        typed + text.upper(w) => typed\n    if typed == \"\":\n        return None\n    [] => ranking\n    for c in typed:\n        0 - 1 => found\n        for i in [0:k - 1]:\n            if letters[i] == c:\n                i => found\n        if found == 0 - 1:\n            return None\n        for o in ranking:\n            if o == found:\n                return None\n        ranking + [found] => ranking\n    return [ranking, n]\n\ndef ranking_text(ranking):\n    \"\" => out\n    for p in [0:len(ranking) - 1]:\n        if p > 0:\n            out + \" > \" => out\n        out + letters[ranking[p]] => out\n    return out\n\ndef show_results(options, ballots):\n    len(options) => k\n    count.total(ballots) => n\n    \"\" ^0\n    (\"-- results: \" + plural(n, \"ballot\") + \" --\") ^0\n    \"Plurality - first choices only:\" ^0\n    count.plurality(ballots, k) => votes\n    for i in ranked(votes, k):\n        (\"  \" + letters[i] + \") \" + (\"%-20s\" % options[i]) + (\"%6d\" % votes[i]) + (\"%8s\" % (text.tenths(100 * votes[i], n) + \"%\"))) ^0\n    count.leaders(votes, k) => top\n    if len(top) == 1:\n        (\"  winner: \" + options[top[0]]) ^0\n    else:\n        (\"  tie: \" + names_text(options, top)) ^0\n    [names_text(options, top)] => winners\n    \"Instant runoff - the last goes out and its ballots move to their next choice:\" ^0\n    count.runoff(ballots, k) => r\n    for i in [0:len(r[0]) - 1]:\n        r[0][i] => rd\n        \"\" => line\n        [] => still_in\n        for o in ranked(rd[0], k):\n            if not rd[3][o]:\n                still_in + [o] => still_in\n        for o in still_in:\n            if line != \"\":\n                line + \", \" => line\n            line + options[o] + \" \" + str(rd[0][o]) => line\n        if rd[2] == 0 - 1:\n            line + \" - \" + options[r[1]] + \" has more than half of \" + str(rd[1]) => line\n        else:\n            line + \" - \" + options[rd[2]] + \" goes out\" => line\n        if rd[1] < n:\n            line + \" (\" + plural(n - rd[1], \"ballot\") + \" with no choice left)\" => line\n        (\"  round \" + str(i + 1) + \": \" + line) ^0\n    (\"  winner: \" + options[r[1]]) ^0\n    winners + [options[r[1]]] => winners\n    (\"Borda - \" + plural(k - 1, \"point\") + \" for first place, one less for each place below:\") ^0\n    count.borda(ballots, k) => points\n    \"\" => line\n    for i in ranked(points, k):\n        if line != \"\":\n            line + \", \" => line\n        line + options[i] + \" \" + str(points[i]) => line\n    (\"  \" + line) ^0\n    count.leaders(points, k) => top\n    if len(top) == 1:\n        (\"  winner: \" + options[top[0]]) ^0\n    else:\n        (\"  tie: \" + names_text(options, top)) ^0\n    winners + [names_text(options, top)] => winners\n    \"Head to head - every pair of options, one against the other:\" ^0\n    count.head_to_head(ballots, k) => wins\n    for i in [0:k - 2]:\n        for j in [i + 1:k - 1]:\n            if wins[i][j] > wins[j][i]:\n                (\"  \" + options[i] + \" beats \" + options[j] + \" \" + str(wins[i][j]) + \" to \" + str(wins[j][i])) ^0\n            elif wins[j][i] > wins[i][j]:\n                (\"  \" + options[j] + \" beats \" + options[i] + \" \" + str(wins[j][i]) + \" to \" + str(wins[i][j])) ^0\n            else:\n                (\"  \" + options[i] + \" and \" + options[j] + \" tie \" + str(wins[i][j]) + \" to \" + str(wins[j][i])) ^0\n    count.beats_all(wins, k) => w\n    if w == 0 - 1:\n        \"  no option beats every other one\" ^0\n        winners + [\"none\"] => winners\n    else:\n        (\"  winner: \" + options[w] + \", who beats every other option\") ^0\n        winners + [options[w]] => winners\n    (\"Winners: plurality \" + winners[0] + \"; instant runoff \" + winners[1] + \"; Borda \" + winners[2] + \"; head to head \" + winners[3] + \".\") ^0\n\n\"\" => question\n[] => options\n[] => ballots\nTrue => running\nwhile running:\n    \"\" ^0\n    \"== Poll ==\" ^0\n    \"1) new poll  2) vote  3) results  4) ballots  5) quit\" ^0\n    text.trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        ask_text(\"question> \", 60) => q\n        if q == \"\":\n            \"Cancelled.\" ^0\n        else:\n            (\"Options, one per line - 2 to \" + str(most_options) + \"; an empty line ends the list.\") ^0\n            [] => new_options\n            while len(new_options) < most_options:\n                ask_text(\"option \" + letters[len(new_options)] + \"> \", 20) => name\n                if name == \"\":\n                    break\n                False => clash\n                for o in new_options:\n                    if text.upper(o) == text.upper(name):\n                        True => clash\n                if clash:\n                    \"That option is already there.\" ^0\n                else:\n                    new_options + [name] => new_options\n            if len(new_options) < 2:\n                \"A poll needs at least two options; nothing changed.\" ^0\n            else:\n                q => question\n                new_options => options\n                [] => ballots\n                \"\" => listing\n                for i in [0:len(options) - 1]:\n                    listing + \"  \" + letters[i] + \") \" + options[i] => listing\n                (\"Poll ready: \" + question) ^0\n                listing ^0\n    elif choice == \"2\":\n        if len(options) == 0:\n            \"Set up a poll first.\" ^0\n        else:\n            (\"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\n            while True:\n                text.trim(input(\"ballot> \")) => answer\n                if answer == \"\":\n                    break\n                parse_ballot(answer, len(options)) => b\n                if b == None:\n                    (\"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\n                else:\n                    ballots + [b] => ballots\n                    (\"Counted \" + plural(b[1], \"ballot\") + \": \" + ranking_text(b[0]) + \".\") ^0\n            (plural(count.total(ballots), \"ballot\") + \" so far.\") ^0\n    elif choice == \"3\":\n        if count.total(ballots) == 0:\n            \"No ballots yet.\" ^0\n        else:\n            (\"Question: \" + question) ^0\n            show_results(options, ballots)\n    elif choice == \"4\":\n        if count.total(ballots) == 0:\n            \"No ballots yet.\" ^0\n        else:\n            [] => groups\n            for b in ballots:\n                False => merged\n                for g in groups:\n                    if not merged and ranking_text(g[0]) == ranking_text(b[0]):\n                        g[1] + b[1] => g[1]\n                        True => merged\n                if not merged:\n                    groups + [[b[0], b[1]]] => groups\n            \"\" ^0\n            (\"-- ballots: \" + plural(count.total(ballots), \"ballot\") + \", \" + plural(len(groups), \"different ranking\") + \" --\") ^0\n            for g in groups:\n                ((\"%6d\" % g[1]) + \"  \" + ranking_text(g[0])) ^0\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import count\nimport text\nletters = \"ABCDEF\"\nmost_options = 6\nmost_at_once = 1000\n\ndef plural(n, word):\n    if n == 1:\n        return \"1 \" + word\n    return str(n) + \" \" + word + \"s\"\n\ndef names_text(options, items):\n    if len(items) == 1:\n        return options[items[0]]\n    out = \"\"\n    for i in range(0, len(items) - 2+1):\n        if i > 0:\n            out = out + \", \"\n        out = out + options[items[i]]\n    return out + \" and \" + options[items[len(items) - 1]]\n\ndef ranked(scores, k):\n    out = []\n    for i in range(0, k):\n        p = len(out)\n        out = out + [i]\n        while p > 0 and scores[out[p - 1]] < scores[i]:\n            out[p] = out[p - 1]\n            p = p - 1\n        out[p] = i\n    return out\n\ndef ask_text(prompt, longest):\n    while True:\n        answer = text.trim(input(prompt))\n        if len(answer) <= longest:\n            return answer\n        print(\"At most \" + str(longest) + \" characters.\")\n\ndef parse_ballot(answer, k):\n    ws = text.words(answer)\n    n = 1\n    if len(ws) > 1 and text.number(ws[0]) >= 0:\n        n = text.number(ws[0])\n        ws = ws[1:len(ws)]\n    if n < 1 or n > most_at_once:\n        return None\n    typed = \"\"\n    for w in ws:\n        typed = typed + text.upper(w)\n    if typed == \"\":\n        return None\n    ranking = []\n    for c in typed:\n        found = 0 - 1\n        for i in range(0, k):\n            if letters[i] == c:\n                found = i\n        if found == 0 - 1:\n            return None\n        for o in ranking:\n            if o == found:\n                return None\n        ranking = ranking + [found]\n    return [ranking, n]\n\ndef ranking_text(ranking):\n    out = \"\"\n    for p in range(0, len(ranking)):\n        if p > 0:\n            out = out + \" > \"\n        out = out + letters[ranking[p]]\n    return out\n\ndef show_results(options, ballots):\n    k = len(options)\n    n = count.total(ballots)\n    print(\"\")\n    print(\"-- results: \" + plural(n, \"ballot\") + \" --\")\n    print(\"Plurality - first choices only:\")\n    votes = count.plurality(ballots, k)\n    for i in ranked(votes, k):\n        print(\"  \" + letters[i] + \") \" + \"%-20s\" % options[i] + \"%6d\" % votes[i] + \"%8s\" % (text.tenths(100 * votes[i], n) + \"%\"))\n    top = count.leaders(votes, k)\n    if len(top) == 1:\n        print(\"  winner: \" + options[top[0]])\n    else:\n        print(\"  tie: \" + names_text(options, top))\n    winners = [names_text(options, top)]\n    print(\"Instant runoff - the last goes out and its ballots move to their next choice:\")\n    r = count.runoff(ballots, k)\n    for i in range(0, len(r[0])):\n        rd = r[0][i]\n        line = \"\"\n        still_in = []\n        for o in ranked(rd[0], k):\n            if not rd[3][o]:\n                still_in = still_in + [o]\n        for o in still_in:\n            if line != \"\":\n                line = line + \", \"\n            line = line + options[o] + \" \" + str(rd[0][o])\n        if rd[2] == 0 - 1:\n            line = line + \" - \" + options[r[1]] + \" has more than half of \" + str(rd[1])\n        else:\n            line = line + \" - \" + options[rd[2]] + \" goes out\"\n        if rd[1] < n:\n            line = line + \" (\" + plural(n - rd[1], \"ballot\") + \" with no choice left)\"\n        print(\"  round \" + str(i + 1) + \": \" + line)\n    print(\"  winner: \" + options[r[1]])\n    winners = winners + [options[r[1]]]\n    print(\"Borda - \" + plural(k - 1, \"point\") + \" for first place, one less for each place below:\")\n    points = count.borda(ballots, k)\n    line = \"\"\n    for i in ranked(points, k):\n        if line != \"\":\n            line = line + \", \"\n        line = line + options[i] + \" \" + str(points[i])\n    print(\"  \" + line)\n    top = count.leaders(points, k)\n    if len(top) == 1:\n        print(\"  winner: \" + options[top[0]])\n    else:\n        print(\"  tie: \" + names_text(options, top))\n    winners = winners + [names_text(options, top)]\n    print(\"Head to head - every pair of options, one against the other:\")\n    wins = count.head_to_head(ballots, k)\n    for i in range(0, k - 2+1):\n        for j in range(i + 1, k):\n            if wins[i][j] > wins[j][i]:\n                print(\"  \" + options[i] + \" beats \" + options[j] + \" \" + str(wins[i][j]) + \" to \" + str(wins[j][i]))\n            elif wins[j][i] > wins[i][j]:\n                print(\"  \" + options[j] + \" beats \" + options[i] + \" \" + str(wins[j][i]) + \" to \" + str(wins[i][j]))\n            else:\n                print(\"  \" + options[i] + \" and \" + options[j] + \" tie \" + str(wins[i][j]) + \" to \" + str(wins[j][i]))\n    w = count.beats_all(wins, k)\n    if w == 0 - 1:\n        print(\"  no option beats every other one\")\n        winners = winners + [\"none\"]\n    else:\n        print(\"  winner: \" + options[w] + \", who beats every other option\")\n        winners = winners + [options[w]]\n    print(\"Winners: plurality \" + winners[0] + \"; instant runoff \" + winners[1] + \"; Borda \" + winners[2] + \"; head to head \" + winners[3] + \".\")\n\nquestion = \"\"\noptions = []\nballots = []\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"== Poll ==\")\n    print(\"1) new poll  2) vote  3) results  4) ballots  5) quit\")\n    choice = text.trim(input(\"choice> \"))\n    if choice == \"1\":\n        q = ask_text(\"question> \", 60)\n        if q == \"\":\n            print(\"Cancelled.\")\n        else:\n            print(\"Options, one per line - 2 to \" + str(most_options) + \"; an empty line ends the list.\")\n            new_options = []\n            while len(new_options) < most_options:\n                name = ask_text(\"option \" + letters[len(new_options)] + \"> \", 20)\n                if name == \"\":\n                    break\n                clash = False\n                for o in new_options:\n                    if text.upper(o) == text.upper(name):\n                        clash = True\n                if clash:\n                    print(\"That option is already there.\")\n                else:\n                    new_options = new_options + [name]\n            if len(new_options) < 2:\n                print(\"A poll needs at least two options; nothing changed.\")\n            else:\n                question = q\n                options = new_options\n                ballots = []\n                listing = \"\"\n                for i in range(0, len(options)):\n                    listing = listing + \"  \" + letters[i] + \") \" + options[i]\n                print(\"Poll ready: \" + question)\n                print(listing)\n    elif choice == \"2\":\n        if len(options) == 0:\n            print(\"Set up a poll first.\")\n        else:\n            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.\")\n            while True:\n                answer = text.trim(input(\"ballot> \"))\n                if answer == \"\":\n                    break\n                b = parse_ballot(answer, len(options))\n                if b == None:\n                    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.\")\n                else:\n                    ballots = ballots + [b]\n                    print(\"Counted \" + plural(b[1], \"ballot\") + \": \" + ranking_text(b[0]) + \".\")\n            print(plural(count.total(ballots), \"ballot\") + \" so far.\")\n    elif choice == \"3\":\n        if count.total(ballots) == 0:\n            print(\"No ballots yet.\")\n        else:\n            print(\"Question: \" + question)\n            show_results(options, ballots)\n    elif choice == \"4\":\n        if count.total(ballots) == 0:\n            print(\"No ballots yet.\")\n        else:\n            groups = []\n            for b in ballots:\n                merged = False\n                for g in groups:\n                    if not merged and ranking_text(g[0]) == ranking_text(b[0]):\n                        g[1] = g[1] + b[1]\n                        merged = True\n                if not merged:\n                    groups = groups + [[b[0], b[1]]]\n            print(\"\")\n            print(\"-- ballots: \" + plural(count.total(ballots), \"ballot\") + \", \" + plural(len(groups), \"different ranking\") + \" --\")\n            for g in groups:\n                print(\"%6d\" % g[1] + \"  \" + ranking_text(g[0]))\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "count.eml",
      "eml": "# P019 poll - four ways to count the same ranked ballots. A ballot group is\n# [ranking, count]: the ranking lists option numbers, most preferred first,\n# and need not rank every option; count is how many identical ballots it\n# stands for. Every rule that could tie says how the tie is broken.\n\ndef total(ballots):\n    0 => n\n    for b in ballots:\n        n + b[1] => n\n    return n\n\ndef first_choices(ballots, k, out_of):\n    # How many ballots put each of the k options first among those still in\n    # (out_of[i] True means option i is out). A ballot with nobody left is\n    # exhausted and counts for no one.\n    [] => votes\n    for i in [0:k - 1]:\n        votes + [0] => votes\n    for b in ballots:\n        0 - 1 => top\n        for o in b[0]:\n            if top == 0 - 1 and not out_of[o]:\n                o => top\n        if top != 0 - 1:\n            votes[top] + b[1] => votes[top]\n    return votes\n\ndef plurality(ballots, k):\n    [] => none_out\n    for i in [0:k - 1]:\n        none_out + [False] => none_out\n    return first_choices(ballots, k, none_out)\n\ndef runoff(ballots, k):\n    # Instant runoff: count first choices among the options still in; an\n    # option with more than half of the ballots still counting wins;\n    # otherwise the option with the fewest goes out and its ballots move to\n    # their next choice. A tie for fewest is broken by the fewest first\n    # choices in the first round, then by the option listed later. Returns\n    # [rounds, winner], a round being [votes, counting, out, gone]: out is\n    # the option that goes out (-1 in the round that has a winner) and gone[i]\n    # says whether option i was already out when the round was counted.\n    [] => out_of\n    for i in [0:k - 1]:\n        out_of + [False] => out_of\n    plurality(ballots, k) => first_round\n    [] => rounds\n    while True:\n        first_choices(ballots, k, out_of) => votes\n        0 => counting\n        0 => left\n        0 - 1 => best\n        for i in [0:k - 1]:\n            if not out_of[i]:\n                counting + votes[i] => counting\n                left + 1 => left\n                if best == 0 - 1 or votes[i] > votes[best]:\n                    i => best\n        if 2 * votes[best] > counting or left == 1:\n            rounds + [[votes, counting, 0 - 1, [x for x in out_of]]] => rounds\n            return [rounds, best]\n        0 - 1 => worst\n        for i in [0:k - 1]:\n            if not out_of[i]:\n                if worst == 0 - 1 or votes[i] < votes[worst]:\n                    i => worst\n                elif votes[i] == votes[worst] and first_round[i] <= first_round[worst]:\n                    i => worst\n        rounds + [[votes, counting, worst, [x for x in out_of]]] => rounds\n        True => out_of[worst]\n\ndef borda(ballots, k):\n    # Borda: with k options, first place earns k - 1 points, second k - 2,\n    # and so on down to 0; an option a ballot leaves unranked earns 0.\n    [] => points\n    for i in [0:k - 1]:\n        points + [0] => points\n    for b in ballots:\n        for p in [0:len(b[0]) - 1]:\n            points[b[0][p]] + (k - 1 - p) * b[1] => points[b[0][p]]\n    return points\n\ndef prefers(ranking, i, j):\n    # Whether a ranking puts option i above option j (an unranked option is\n    # below every ranked one).\n    for o in ranking:\n        if o == i:\n            return True\n        if o == j:\n            return False\n    return False\n\ndef head_to_head(ballots, k):\n    # wins[i][j]: how many ballots prefer option i to option j.\n    [] => wins\n    for i in [0:k - 1]:\n        [] => row\n        for j in [0:k - 1]:\n            0 => n\n            if i != j:\n                for b in ballots:\n                    if prefers(b[0], i, j):\n                        n + b[1] => n\n            row + [n] => row\n        wins + [row] => wins\n    return wins\n\ndef beats_all(wins, k):\n    # The option that beats every other head to head, or -1 if there is none.\n    for i in [0:k - 1]:\n        True => all_beaten\n        for j in [0:k - 1]:\n            if i != j and wins[i][j] <= wins[j][i]:\n                False => all_beaten\n        if all_beaten:\n            return i\n    return 0 - 1\n\ndef leaders(scores, k):\n    # The options with the highest score, in option order.\n    0 => best\n    for i in [0:k - 1]:\n        if scores[i] > best:\n            scores[i] => best\n    [] => out\n    for i in [0:k - 1]:\n        if scores[i] == best:\n            out + [i] => out\n    return out\n",
      "python": "def total(ballots):\n    n = 0\n    for b in ballots:\n        n = n + b[1]\n    return n\n\ndef first_choices(ballots, k, out_of):\n    votes = []\n    for i in range(0, k):\n        votes = votes + [0]\n    for b in ballots:\n        top = 0 - 1\n        for o in b[0]:\n            if top == 0 - 1 and not out_of[o]:\n                top = o\n        if top != 0 - 1:\n            votes[top] = votes[top] + b[1]\n    return votes\n\ndef plurality(ballots, k):\n    none_out = []\n    for i in range(0, k):\n        none_out = none_out + [False]\n    return first_choices(ballots, k, none_out)\n\ndef runoff(ballots, k):\n    out_of = []\n    for i in range(0, k):\n        out_of = out_of + [False]\n    first_round = plurality(ballots, k)\n    rounds = []\n    while True:\n        votes = first_choices(ballots, k, out_of)\n        counting = 0\n        left = 0\n        best = 0 - 1\n        for i in range(0, k):\n            if not out_of[i]:\n                counting = counting + votes[i]\n                left = left + 1\n                if best == 0 - 1 or votes[i] > votes[best]:\n                    best = i\n        if 2 * votes[best] > counting or left == 1:\n            rounds = rounds + [[votes, counting, 0 - 1, [x for x in out_of]]]\n            return [rounds, best]\n        worst = 0 - 1\n        for i in range(0, k):\n            if not out_of[i]:\n                if worst == 0 - 1 or votes[i] < votes[worst]:\n                    worst = i\n                elif votes[i] == votes[worst] and first_round[i] <= first_round[worst]:\n                    worst = i\n        rounds = rounds + [[votes, counting, worst, [x for x in out_of]]]\n        out_of[worst] = True\n\ndef borda(ballots, k):\n    points = []\n    for i in range(0, k):\n        points = points + [0]\n    for b in ballots:\n        for p in range(0, len(b[0])):\n            points[b[0][p]] = points[b[0][p]] + (k - 1 - p) * b[1]\n    return points\n\ndef prefers(ranking, i, j):\n    for o in ranking:\n        if o == i:\n            return True\n        if o == j:\n            return False\n    return False\n\ndef head_to_head(ballots, k):\n    wins = []\n    for i in range(0, k):\n        row = []\n        for j in range(0, k):\n            n = 0\n            if i != j:\n                for b in ballots:\n                    if prefers(b[0], i, j):\n                        n = n + b[1]\n            row = row + [n]\n        wins = wins + [row]\n    return wins\n\ndef beats_all(wins, k):\n    for i in range(0, k):\n        all_beaten = True\n        for j in range(0, k):\n            if i != j and wins[i][j] <= wins[j][i]:\n                all_beaten = False\n        if all_beaten:\n            return i\n    return 0 - 1\n\ndef leaders(scores, k):\n    best = 0\n    for i in range(0, k):\n        if scores[i] > best:\n            best = scores[i]\n    out = []\n    for i in range(0, k):\n        if scores[i] == best:\n            out = out + [i]\n    return out\n"
    },
    {
      "name": "text.eml",
      "eml": "# P019 poll - reading what is typed. The interpreter that checks every\n# session does not run string methods yet, so the text handling is written\n# out here.\n\ndef trim(s):\n    # s without the spaces at either end.\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef upper(s):\n    # s with a-z turned into A-Z; everything else as it was.\n    \"abcdefghijklmnopqrstuvwxyz\" => lower_letters\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" => upper_letters\n    \"\" => out\n    for c in s:\n        c => d\n        for i in [0:25]:\n            if lower_letters[i] == c:\n                upper_letters[i] => d\n        out + d => out\n    return out\n\ndef number(s):\n    # The value of s if it is digits only (at least one), otherwise -1.\n    if s == \"\":\n        return 0 - 1\n    0 => n\n    for c in s:\n        if not (c in \"0123456789\"):\n            return 0 - 1\n        n * 10 + int(c) => n\n    return n\n\ndef words(s):\n    # The words of s, split at spaces and commas.\n    [] => out\n    \"\" => word\n    for c in s:\n        if c == \" \" or c == \",\":\n            if word != \"\":\n                out + [word] => out\n            \"\" => word\n        else:\n            word + c => word\n    if word != \"\":\n        out + [word] => out\n    return out\n\ndef tenths(n, d):\n    # n / d to one decimal, rounded half up, as text: 2 / 3 is \"0.7\".\n    2 * 10 * n + d => t\n    int((t - t % (2 * d)) / (2 * d)) => v\n    return str(int((v - v % 10) / 10)) + \".\" + str(v % 10)\n",
      "python": "def trim(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef upper(s):\n    lower_letters = \"abcdefghijklmnopqrstuvwxyz\"\n    upper_letters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    out = \"\"\n    for c in s:\n        d = c\n        for i in range(0, 26):\n            if lower_letters[i] == c:\n                d = upper_letters[i]\n        out = out + d\n    return out\n\ndef number(s):\n    if s == \"\":\n        return 0 - 1\n    n = 0\n    for c in s:\n        if not c in \"0123456789\":\n            return 0 - 1\n        n = n * 10 + int(c)\n    return n\n\ndef words(s):\n    out = []\n    word = \"\"\n    for c in s:\n        if c == \" \" or c == \",\":\n            if word != \"\":\n                out = out + [word]\n            word = \"\"\n        else:\n            word = word + c\n    if word != \"\":\n        out = out + [word]\n    return out\n\ndef tenths(n, d):\n    t = 2 * 10 * n + d\n    v = int((t - t % (2 * d)) / (2 * d))\n    return str(int((v - v % 10) / 10)) + \".\" + str(v % 10)\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "7\n2\n3\n4\n1\n\n1\nA question that is far too long to fit in the sixty characters allowed\nIs it fine?\nYes\nyes\n\n1\nTea or coffee?\nTea\nCoffee\nA name that is far too long\nWater\n\n3\n2\nD\nA A\n0 A\n1001 B\nx\n3 a\nc,b\nc\n2 b\n\n4\n3\n5\n",
      "screen": "\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 7\nPick a number from 1 to 5.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 2\nSet up a poll first.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 3\nNo ballots yet.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 4\nNo ballots yet.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 1\nquestion> \nCancelled.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 1\nquestion> A question that is far too long to fit in the sixty characters allowed\nAt most 60 characters.\nquestion> Is it fine?\nOptions, one per line - 2 to 6; an empty line ends the list.\noption A> Yes\noption B> yes\nThat option is already there.\noption B> \nA poll needs at least two options; nothing changed.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 1\nquestion> Tea or coffee?\nOptions, one per line - 2 to 6; an empty line ends the list.\noption A> Tea\noption B> Coffee\noption C> A name that is far too long\nAt most 20 characters.\noption C> Water\noption D> \nPoll ready: Tea or coffee?\n  A) Tea  B) Coffee  C) Water\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 3\nNo ballots yet.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 2\nRank 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.\nballot> D\nType letters from A to C, each once, with a count from 1 to 1000 in front if you like.\nballot> A A\nType letters from A to C, each once, with a count from 1 to 1000 in front if you like.\nballot> 0 A\nType letters from A to C, each once, with a count from 1 to 1000 in front if you like.\nballot> 1001 B\nType letters from A to C, each once, with a count from 1 to 1000 in front if you like.\nballot> x\nType letters from A to C, each once, with a count from 1 to 1000 in front if you like.\nballot> 3 a\nCounted 3 ballots: A.\nballot> c,b\nCounted 1 ballot: C > B.\nballot> c\nCounted 1 ballot: C.\nballot> 2 b\nCounted 2 ballots: B.\nballot> \n7 ballots so far.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 4\n\n-- ballots: 7 ballots, 4 different rankings --\n     3  A\n     1  C > B\n     1  C\n     2  B\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 3\nQuestion: Tea or coffee?\n\n-- results: 7 ballots --\nPlurality - first choices only:\n  A) Tea                      3   42.9%\n  B) Coffee                   2   28.6%\n  C) Water                    2   28.6%\n  winner: Tea\nInstant runoff - the last goes out and its ballots move to their next choice:\n  round 1: Tea 3, Coffee 2, Water 2 - Water goes out\n  round 2: Tea 3, Coffee 3 - Coffee goes out (1 ballot with no choice left)\n  round 3: Tea 3 - Tea has more than half of 3 (4 ballots with no choice left)\n  winner: Tea\nBorda - 2 points for first place, one less for each place below:\n  Tea 6, Coffee 5, Water 4\n  winner: Tea\nHead to head - every pair of options, one against the other:\n  Tea and Coffee tie 3 to 3\n  Tea beats Water 3 to 2\n  Coffee and Water tie 2 to 2\n  no option beats every other one\nWinners: plurality Tea; instant runoff Tea; Borda Tea; head to head none.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\nWhere should the new library go?\nRiverside\nHilltop\nDowntown\n\n2\n40 A B C\n35 C B A\n25 B A C\n\n3\n4\n5\n",
      "screen": "\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 1\nquestion> Where should the new library go?\nOptions, one per line - 2 to 6; an empty line ends the list.\noption A> Riverside\noption B> Hilltop\noption C> Downtown\noption D> \nPoll ready: Where should the new library go?\n  A) Riverside  B) Hilltop  C) Downtown\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 2\nRank 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.\nballot> 40 A B C\nCounted 40 ballots: A > B > C.\nballot> 35 C B A\nCounted 35 ballots: C > B > A.\nballot> 25 B A C\nCounted 25 ballots: B > A > C.\nballot> \n100 ballots so far.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 3\nQuestion: Where should the new library go?\n\n-- results: 100 ballots --\nPlurality - first choices only:\n  A) Riverside               40   40.0%\n  C) Downtown                35   35.0%\n  B) Hilltop                 25   25.0%\n  winner: Riverside\nInstant runoff - the last goes out and its ballots move to their next choice:\n  round 1: Riverside 40, Downtown 35, Hilltop 25 - Hilltop goes out\n  round 2: Riverside 65, Downtown 35 - Riverside has more than half of 100\n  winner: Riverside\nBorda - 2 points for first place, one less for each place below:\n  Hilltop 125, Riverside 105, Downtown 70\n  winner: Hilltop\nHead to head - every pair of options, one against the other:\n  Hilltop beats Riverside 60 to 40\n  Riverside beats Downtown 65 to 35\n  Hilltop beats Downtown 65 to 35\n  winner: Hilltop, who beats every other option\nWinners: plurality Riverside; instant runoff Riverside; Borda Hilltop; head to head Hilltop.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 4\n\n-- ballots: 100 ballots, 3 different rankings --\n    40  A > B > C\n    35  C > B > A\n    25  B > A > C\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "strategic",
      "input": "1\nWhich design should the club pick?\nAlder\nBirch\nCedar\n\n2\n55 A B C\n45 B C A\n\n3\n1\nWhich design should the club pick?\nAlder\nBirch\nCedar\n\n2\n55 A C B\n45 B C A\n\n3\n5\n",
      "screen": "\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 1\nquestion> Which design should the club pick?\nOptions, one per line - 2 to 6; an empty line ends the list.\noption A> Alder\noption B> Birch\noption C> Cedar\noption D> \nPoll ready: Which design should the club pick?\n  A) Alder  B) Birch  C) Cedar\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 2\nRank 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.\nballot> 55 A B C\nCounted 55 ballots: A > B > C.\nballot> 45 B C A\nCounted 45 ballots: B > C > A.\nballot> \n100 ballots so far.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 3\nQuestion: Which design should the club pick?\n\n-- results: 100 ballots --\nPlurality - first choices only:\n  A) Alder                   55   55.0%\n  B) Birch                   45   45.0%\n  C) Cedar                    0    0.0%\n  winner: Alder\nInstant runoff - the last goes out and its ballots move to their next choice:\n  round 1: Alder 55, Birch 45, Cedar 0 - Alder has more than half of 100\n  winner: Alder\nBorda - 2 points for first place, one less for each place below:\n  Birch 145, Alder 110, Cedar 45\n  winner: Birch\nHead to head - every pair of options, one against the other:\n  Alder beats Birch 55 to 45\n  Alder beats Cedar 55 to 45\n  Birch beats Cedar 100 to 0\n  winner: Alder, who beats every other option\nWinners: plurality Alder; instant runoff Alder; Borda Birch; head to head Alder.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 1\nquestion> Which design should the club pick?\nOptions, one per line - 2 to 6; an empty line ends the list.\noption A> Alder\noption B> Birch\noption C> Cedar\noption D> \nPoll ready: Which design should the club pick?\n  A) Alder  B) Birch  C) Cedar\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 2\nRank 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.\nballot> 55 A C B\nCounted 55 ballots: A > C > B.\nballot> 45 B C A\nCounted 45 ballots: B > C > A.\nballot> \n100 ballots so far.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 3\nQuestion: Which design should the club pick?\n\n-- results: 100 ballots --\nPlurality - first choices only:\n  A) Alder                   55   55.0%\n  B) Birch                   45   45.0%\n  C) Cedar                    0    0.0%\n  winner: Alder\nInstant runoff - the last goes out and its ballots move to their next choice:\n  round 1: Alder 55, Birch 45, Cedar 0 - Alder has more than half of 100\n  winner: Alder\nBorda - 2 points for first place, one less for each place below:\n  Alder 110, Cedar 100, Birch 90\n  winner: Alder\nHead to head - every pair of options, one against the other:\n  Alder beats Birch 55 to 45\n  Alder beats Cedar 55 to 45\n  Cedar beats Birch 55 to 45\n  winner: Alder, who beats every other option\nWinners: plurality Alder; instant runoff Alder; Borda Alder; head to head Alder.\n\n== Poll ==\n1) new poll  2) vote  3) results  4) ballots  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "the-runoff-eliminated-the-one-everyone-could-accept",
      "caseId": "878-the-runoff-eliminated-the-one-everyone-could-accept",
      "title": "The runoff eliminated the one everyone could accept"
    },
    {
      "slug": "the-sincere-ballot-lost-to-the-strategic-one",
      "caseId": "881-the-sincere-ballot-lost-to-the-strategic-one",
      "title": "The sincere ballot lost to the strategic one"
    }
  ],
  "updated": "2026-10-03"
}
