{
  "id": "P112",
  "slug": "survey-analyzer",
  "key": "P112-survey-analyzer",
  "title": "Survey analyzer",
  "summary": "A small survey: each question's exact mean, median and spread; the Net Promoter Score and the scores that would move it; the top respondents under strict, dense and exclude policies for ties; and a split by satisfaction.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P112 - Survey analyzer\n\nA small survey: how likely each person is to recommend us (0 to 10), and\nthree questions on a scale of 1 to 5. The program shows the responses,\neach question's average and spread, the Net Promoter Score and what\nmoves it, the top respondents when scores tie, and a split by\nsatisfaction.\n\n- `main.eml` - the menu, the tables and the reports, and adding a\n  response\n- `survey.eml` - exact fractions, medians, the promoter groups, totals,\n  the ranking and the three top-N policies\n\nHow each part works:\n\n- Averages are exact: the mean is shown as a fraction and to two decimals.\n  The median of an even count is the mean of the middle two. \"4 or 5\"\n  counts the top two answers.\n- The Net Promoter Score is the share of promoters (9 or 10) minus the\n  share of detractors (0 to 6), from -100 to +100. A recommend score counts\n  only when it crosses from 6 to 7 or from 8 to 9. The program names who\n  would change the score with one point more, and how many others would\n  move only the mean.\n- The top respondents by the total of q1 to q3, as in the corpus case\n  `top-n-with-ties`:\n  - strict: exactly N, the first N of a ranking that keeps equal totals in\n    their order;\n  - dense: everyone with a total at least the N-th's;\n  - exclude: when the N-th is tied with the next one, only those above the\n    tie.\n\n  Strict depends on the order of the responses, so the program also shows\n  the reverse order whenever that gives other people.\n- Satisfied or not, as in the corpus case `survey-score-analyzer`: a\n  recommend score of 7 or more, against 6 or less.\n\nWhat is checked: menu choices 1 to 7; a top-N count from 1 to the number\nof responses; a new response as a name of up to 6 lower-case letters,\nnot yet in the survey, a recommend score from 0 to 10 and three answers\nfrom 1 to 5; at most 30 responses. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the 12 responses, each question, and the Net Promoter Score of 16.67;\n- the top 3, then the top 4 (strict takes bo, or kai in reverse order;\n  dense keeps both; exclude drops both), then the top 2 (strict takes ana,\n  or fay in reverse order);\n- the split by satisfaction;\n- zed added as a detractor with top answers: the score falls to 7.69, and\n  the top 4 again.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- top-N counts: a cancel, 0, 13 and x;\n- new responses: a cancel, three numbers, a name already in, upper case,\n  a name too long, a recommend score of 11, an answer of 6, and a letter\n  for a number.\n\nBuilt on the verified corpus cases `survey-score-analyzer` (respondents\nsplit by a threshold and averaged) and `top-n-with-ties` (the top N is\nnot defined until a policy for ties is).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P112 survey analyzer: responses, each question's average and spread,\n# the Net Promoter Score and what moves it, and the top respondents when\n# scores tie.\nimport survey\n\ndef plain(s):\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 number(s, most):\n    if len(s) == 0 or len(s) > most:\n        return -1\n    0 => n\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n * 10 + int(c) => n\n    return n\n\ndef pad(s, n):\n    while len(s) < n:\n        s + \" \" => s\n    return s\n\ndef lpad(s, n):\n    while len(s) < n:\n        \" \" + s => s\n    return s\n\ndef names(rows):\n    \"\" => out\n    for r in rows:\n        if out != \"\":\n            out + \", \" => out\n        out + r[0] + \" \" + str(survey.total(r)) => out\n    if out == \"\":\n        return \"(nobody)\"\n    return out\n\ndef show_rows(rows):\n    \"  name   recommend  group      q1 q2 q3  total\" ^0\n    for r in rows:\n        (\"  \" + pad(r[0], 7) + lpad(str(r[1]), 9) + \"  \" + pad(survey.group(r[1]), 10) + lpad(str(r[2]), 2) + lpad(str(r[3]), 3) + lpad(str(r[4]), 3) + lpad(str(survey.total(r)), 7)) ^0\n    (str(len(rows)) + \" responses.\") ^0\n\ndef questions(rows):\n    [\"recommend\", \"q1\", \"q2\", \"q3\"] => labels\n    for k in [1:4]:\n        survey.column(rows, k) => xs\n        0 => s\n        for x in xs:\n            s + x => s\n        survey.median_twice(xs) => m2\n        str(int(m2 / 2)) => med\n        if m2 % 2 == 1:\n            str(int((m2 - 1) / 2)) + \".5\" => med\n        0 => low\n        10 => high\n        if k > 1:\n            1 => low\n            5 => high\n        \"\" => dist\n        for v in [low:high]:\n            0 => c\n            for x in xs:\n                if x == v:\n                    c + 1 => c\n            if c > 0:\n                if dist != \"\":\n                    dist + \"  \" => dist\n                dist + str(v) + \":\" + str(c) => dist\n        (\"  \" + pad(labels[k - 1], 10) + \"mean \" + survey.two_places(s, len(xs)) + \" (\" + survey.fraction_text(s, len(xs)) + \"), median \" + med + \"   \" + dist) ^0\n        if k > 1:\n            0 => box\n            for x in xs:\n                if x >= 4:\n                    box + 1 => box\n            (\"  \" + pad(\"\", 10) + \"answered 4 or 5: \" + str(box) + \" of \" + str(len(xs)) + \", \" + survey.two_places(box * 100, len(xs)) + \"%\") ^0\n\ndef show_nps(rows):\n    survey.nps(rows) => g\n    len(rows) => n\n    (\"Promoters (9-10): \" + str(g[0]) + \", passives (7-8): \" + str(g[1]) + \", detractors (0-6): \" + str(g[2]) + \", of \" + str(n) + \".\") ^0\n    (\"Net Promoter Score: \" + survey.two_places((g[0] - g[2]) * 100, n) + \" (\" + survey.fraction_text((g[0] - g[2]) * 100, n) + \"), on a scale from -100 to +100.\") ^0\n    [] => moves\n    [] => still\n    for r in rows:\n        if r[1] == 6 or r[1] == 8:\n            moves + [r[0] + \" \" + str(r[1]) + \" to \" + str(r[1] + 1)] => moves\n        elif r[1] < 10:\n            still + [r[0]] => still\n    \"\" => a\n    for x in moves:\n        if a != \"\":\n            a + \", \" => a\n        a + x => a\n    if a == \"\":\n        \"nobody\" => a\n    (\"One point more would change the score only for \" + a + \": a recommend score counts only when it crosses 6 to 7 or 8 to 9.\") ^0\n    (str(len(still)) + \" others would raise the mean recommend score and leave the Net Promoter Score as it is.\") ^0\n\ndef show_top(rows):\n    plain(input(\"how many at the top (1 to \" + str(len(rows)) + \")> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    number(t, 2) => n\n    if n < 1 or n > len(rows):\n        (\"Type a whole number from 1 to \" + str(len(rows)) + \".\") ^0\n        return\n    (\"Top \" + str(n) + \" by total of q1 to q3:\") ^0\n    for p in [\"strict\", \"dense\", \"exclude\"]:\n        survey.top(rows, n, p) => a\n        survey.top(survey.reversed_rows(rows), n, p) => b\n        (\"  \" + pad(p, 8) + str(len(a)) + \": \" + names(a)) ^0\n        True => same\n        if len(a) != len(b):\n            False => same\n        else:\n            for x in a:\n                False => found\n                for y in b:\n                    if y[0] == x[0]:\n                        True => found\n                if not found:\n                    False => same\n        if not same:\n            (\"  \" + pad(\"\", 8) + \"with the responses in the reverse order: \" + names(b)) ^0\n    \"Strict gives exactly the number asked for, but when the cut falls inside a tie it keeps whoever came first. Dense keeps the whole tie, exclude drops it.\" ^0\n\ndef split(rows):\n    [] => happy\n    [] => unhappy\n    for r in rows:\n        if r[1] >= 7:\n            happy + [r] => happy\n        else:\n            unhappy + [r] => unhappy\n    for part in [[\"recommend 7 or more\", happy], [\"recommend 6 or less\", unhappy]]:\n        part[1] => g\n        if len(g) == 0:\n            (\"  \" + pad(part[0], 21) + \"nobody\") ^0\n        else:\n            0 => s\n            for r in g:\n                s + survey.total(r) => s\n            (\"  \" + pad(part[0], 21) + str(len(g)) + \", mean total of q1 to q3 \" + survey.two_places(s, len(g))) ^0\n\ndef add(rows):\n    \"A response as name, recommend (0 to 10) and q1 to q3 (1 to 5), such as zed 8 4 4 3.\" ^0\n    plain(input(\"response> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return rows\n    [] => w\n    \"\" => cur\n    for c in t + \" \":\n        if c == \" \":\n            if cur != \"\":\n                w + [cur] => w\n            \"\" => cur\n        else:\n            cur + c => cur\n    if len(w) != 5:\n        \"Type a name and four numbers.\" ^0\n        return rows\n    for r in rows:\n        if r[0] == w[0]:\n            (w[0] + \" has answered already.\") ^0\n            return rows\n    if len(w[0]) > 6:\n        \"Keep a name to 6 letters.\" ^0\n        return rows\n    for c in w[0]:\n        if not (c >= \"a\" and c <= \"z\"):\n            \"A name is lower-case letters.\" ^0\n            return rows\n    number(w[1], 2) => rec\n    if rec < 0 or rec > 10:\n        \"The recommend score is a whole number from 0 to 10.\" ^0\n        return rows\n    [] => q\n    for k in [2:4]:\n        number(w[k], 1) => v\n        if v < 1 or v > 5:\n            \"q1 to q3 are whole numbers from 1 to 5.\" ^0\n            return rows\n        q + [v] => q\n    if len(rows) >= 30:\n        \"The survey holds 30 responses.\" ^0\n        return rows\n    (w[0] + \" added.\") ^0\n    return rows + [[w[0], rec, q[0], q[1], q[2]]]\n\n[[\"ana\", 10, 5, 4, 5], [\"bo\", 9, 4, 4, 4], [\"cy\", 8, 4, 3, 4], [\"dee\", 7, 3, 4, 3], [\"eli\", 6, 3, 3, 2], [\"fay\", 9, 5, 5, 4], [\"gus\", 3, 2, 1, 2], [\"hal\", 10, 5, 5, 5], [\"ivy\", 8, 4, 4, 3], [\"jo\", 5, 2, 3, 3], [\"kai\", 9, 4, 4, 4], [\"lu\", 7, 4, 3, 4]] => rows\n\n\"== Survey analyzer ==\" ^0\n\"A survey: how likely each person is to recommend us (0 to 10), and three questions on a scale of 1 to 5.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_rows(rows)\n    elif choice == \"2\":\n        questions(rows)\n    elif choice == \"3\":\n        show_nps(rows)\n    elif choice == \"4\":\n        show_top(rows)\n    elif choice == \"5\":\n        split(rows)\n    elif choice == \"6\":\n        add(rows) => rows\n    elif choice == \"7\":\n        False => running\n    else:\n        \"Pick a number from 1 to 7.\" ^0\n\"Bye.\" ^0\n",
      "python": "import survey\n\ndef plain(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 number(s, most):\n    if len(s) == 0 or len(s) > most:\n        return -1\n    n = 0\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n = n * 10 + int(c)\n    return n\n\ndef pad(s, n):\n    while len(s) < n:\n        s = s + \" \"\n    return s\n\ndef lpad(s, n):\n    while len(s) < n:\n        s = \" \" + s\n    return s\n\ndef names(rows):\n    out = \"\"\n    for r in rows:\n        if out != \"\":\n            out = out + \", \"\n        out = out + r[0] + \" \" + str(survey.total(r))\n    if out == \"\":\n        return \"(nobody)\"\n    return out\n\ndef show_rows(rows):\n    print(\"  name   recommend  group      q1 q2 q3  total\")\n    for r in rows:\n        print(\"  \" + pad(r[0], 7) + lpad(str(r[1]), 9) + \"  \" + pad(survey.group(r[1]), 10) + lpad(str(r[2]), 2) + lpad(str(r[3]), 3) + lpad(str(r[4]), 3) + lpad(str(survey.total(r)), 7))\n    print(str(len(rows)) + \" responses.\")\n\ndef questions(rows):\n    labels = [\"recommend\", \"q1\", \"q2\", \"q3\"]\n    for k in range(1, 5):\n        xs = survey.column(rows, k)\n        s = 0\n        for x in xs:\n            s = s + x\n        m2 = survey.median_twice(xs)\n        med = str(int(m2 / 2))\n        if m2 % 2 == 1:\n            med = str(int((m2 - 1) / 2)) + \".5\"\n        low = 0\n        high = 10\n        if k > 1:\n            low = 1\n            high = 5\n        dist = \"\"\n        for v in range(low, high+1):\n            c = 0\n            for x in xs:\n                if x == v:\n                    c = c + 1\n            if c > 0:\n                if dist != \"\":\n                    dist = dist + \"  \"\n                dist = dist + str(v) + \":\" + str(c)\n        print(\"  \" + pad(labels[k - 1], 10) + \"mean \" + survey.two_places(s, len(xs)) + \" (\" + survey.fraction_text(s, len(xs)) + \"), median \" + med + \"   \" + dist)\n        if k > 1:\n            box = 0\n            for x in xs:\n                if x >= 4:\n                    box = box + 1\n            print(\"  \" + pad(\"\", 10) + \"answered 4 or 5: \" + str(box) + \" of \" + str(len(xs)) + \", \" + survey.two_places(box * 100, len(xs)) + \"%\")\n\ndef show_nps(rows):\n    g = survey.nps(rows)\n    n = len(rows)\n    print(\"Promoters (9-10): \" + str(g[0]) + \", passives (7-8): \" + str(g[1]) + \", detractors (0-6): \" + str(g[2]) + \", of \" + str(n) + \".\")\n    print(\"Net Promoter Score: \" + survey.two_places((g[0] - g[2]) * 100, n) + \" (\" + survey.fraction_text((g[0] - g[2]) * 100, n) + \"), on a scale from -100 to +100.\")\n    moves = []\n    still = []\n    for r in rows:\n        if r[1] == 6 or r[1] == 8:\n            moves = moves + [r[0] + \" \" + str(r[1]) + \" to \" + str(r[1] + 1)]\n        elif r[1] < 10:\n            still = still + [r[0]]\n    a = \"\"\n    for x in moves:\n        if a != \"\":\n            a = a + \", \"\n        a = a + x\n    if a == \"\":\n        a = \"nobody\"\n    print(\"One point more would change the score only for \" + a + \": a recommend score counts only when it crosses 6 to 7 or 8 to 9.\")\n    print(str(len(still)) + \" others would raise the mean recommend score and leave the Net Promoter Score as it is.\")\n\ndef show_top(rows):\n    t = plain(input(\"how many at the top (1 to \" + str(len(rows)) + \")> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    n = number(t, 2)\n    if n < 1 or n > len(rows):\n        print(\"Type a whole number from 1 to \" + str(len(rows)) + \".\")\n        return\n    print(\"Top \" + str(n) + \" by total of q1 to q3:\")\n    for p in [\"strict\", \"dense\", \"exclude\"]:\n        a = survey.top(rows, n, p)\n        b = survey.top(survey.reversed_rows(rows), n, p)\n        print(\"  \" + pad(p, 8) + str(len(a)) + \": \" + names(a))\n        same = True\n        if len(a) != len(b):\n            same = False\n        else:\n            for x in a:\n                found = False\n                for y in b:\n                    if y[0] == x[0]:\n                        found = True\n                if not found:\n                    same = False\n        if not same:\n            print(\"  \" + pad(\"\", 8) + \"with the responses in the reverse order: \" + names(b))\n    print(\"Strict gives exactly the number asked for, but when the cut falls inside a tie it keeps whoever came first. Dense keeps the whole tie, exclude drops it.\")\n\ndef split(rows):\n    happy = []\n    unhappy = []\n    for r in rows:\n        if r[1] >= 7:\n            happy = happy + [r]\n        else:\n            unhappy = unhappy + [r]\n    for part in [[\"recommend 7 or more\", happy], [\"recommend 6 or less\", unhappy]]:\n        g = part[1]\n        if len(g) == 0:\n            print(\"  \" + pad(part[0], 21) + \"nobody\")\n        else:\n            s = 0\n            for r in g:\n                s = s + survey.total(r)\n            print(\"  \" + pad(part[0], 21) + str(len(g)) + \", mean total of q1 to q3 \" + survey.two_places(s, len(g)))\n\ndef add(rows):\n    print(\"A response as name, recommend (0 to 10) and q1 to q3 (1 to 5), such as zed 8 4 4 3.\")\n    t = plain(input(\"response> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return rows\n    w = []\n    cur = \"\"\n    for c in t + \" \":\n        if c == \" \":\n            if cur != \"\":\n                w = w + [cur]\n            cur = \"\"\n        else:\n            cur = cur + c\n    if len(w) != 5:\n        print(\"Type a name and four numbers.\")\n        return rows\n    for r in rows:\n        if r[0] == w[0]:\n            print(w[0] + \" has answered already.\")\n            return rows\n    if len(w[0]) > 6:\n        print(\"Keep a name to 6 letters.\")\n        return rows\n    for c in w[0]:\n        if not (c >= \"a\" and c <= \"z\"):\n            print(\"A name is lower-case letters.\")\n            return rows\n    rec = number(w[1], 2)\n    if rec < 0 or rec > 10:\n        print(\"The recommend score is a whole number from 0 to 10.\")\n        return rows\n    q = []\n    for k in range(2, 5):\n        v = number(w[k], 1)\n        if v < 1 or v > 5:\n            print(\"q1 to q3 are whole numbers from 1 to 5.\")\n            return rows\n        q = q + [v]\n    if len(rows) >= 30:\n        print(\"The survey holds 30 responses.\")\n        return rows\n    print(w[0] + \" added.\")\n    return rows + [[w[0], rec, q[0], q[1], q[2]]]\n\nrows = [[\"ana\", 10, 5, 4, 5], [\"bo\", 9, 4, 4, 4], [\"cy\", 8, 4, 3, 4], [\"dee\", 7, 3, 4, 3], [\"eli\", 6, 3, 3, 2], [\"fay\", 9, 5, 5, 4], [\"gus\", 3, 2, 1, 2], [\"hal\", 10, 5, 5, 5], [\"ivy\", 8, 4, 4, 3], [\"jo\", 5, 2, 3, 3], [\"kai\", 9, 4, 4, 4], [\"lu\", 7, 4, 3, 4]]\nprint(\"== Survey analyzer ==\")\nprint(\"A survey: how likely each person is to recommend us (0 to 10), and three questions on a scale of 1 to 5.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_rows(rows)\n    elif choice == \"2\":\n        questions(rows)\n    elif choice == \"3\":\n        show_nps(rows)\n    elif choice == \"4\":\n        show_top(rows)\n    elif choice == \"5\":\n        split(rows)\n    elif choice == \"6\":\n        rows = add(rows)\n    elif choice == \"7\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 7.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "survey.eml",
      "eml": "# P112 survey analyzer - each response is [name, recommend 0-10, q1, q2,\n# q3] with q1-q3 on a 1-5 scale. Averages are exact fractions; the Net\n# Promoter Score is the share of promoters (9-10) minus the share of\n# detractors (0-6). As in the corpus case survey-score-analyzer,\n# respondents split at a threshold; as in the corpus case top-n-with-ties,\n# \"the top N\" on scores with ties depends on a policy - strict, dense or\n# exclude - and strict's answer depends on the order of the input.\n\ndef gcd(a, b):\n    while b != 0:\n        a % b => r\n        b => a\n        r => b\n    return a\n\ndef fraction_text(num, den):\n    # \"49/12\", \"4\" or \"-1/3\"\n    \"\" => sign\n    num => a\n    if num < 0:\n        \"-\" => sign\n        0 - num => a\n    gcd(a, den) => g\n    if g == 0:\n        1 => g\n    int(a / g) => a\n    int(den / g) => d\n    if d == 1:\n        return sign + str(a)\n    return sign + str(a) + \"/\" + str(d)\n\ndef two_places(num, den):\n    # num / den to two decimals, halves away from zero\n    \"\" => sign\n    num => a\n    if num < 0:\n        \"-\" => sign\n        0 - num => a\n    a * 100 => x\n    x % den => r\n    int((x - r) / den) => q\n    if 2 * r >= den:\n        q + 1 => q\n    q % 100 => c\n    int((q - c) / 100) => w\n    if c < 10:\n        return sign + str(w) + \".0\" + str(c)\n    return sign + str(w) + \".\" + str(c)\n\ndef column(rows, k):\n    [] => out\n    for r in rows:\n        out + [r[k]] => out\n    return out\n\ndef sorted_ints(xs):\n    [] => out\n    for x in xs:\n        out + [x] => out\n    for i in [1:len(out) - 1]:\n        i => j\n        while j > 0 and out[j] < out[j - 1]:\n            out[j] => t\n            out[j - 1] => out[j]\n            t => out[j - 1]\n            j - 1 => j\n    return out\n\ndef median_twice(xs):\n    # twice the median, so it stays whole\n    sorted_ints(xs) => s\n    len(s) => n\n    if n % 2 == 1:\n        return 2 * s[int((n - 1) / 2)]\n    return s[int(n / 2) - 1] + s[int(n / 2)]\n\ndef nps(rows):\n    # [promoters, passives, detractors]\n    0 => p\n    0 => m\n    0 => d\n    for r in rows:\n        if r[1] >= 9:\n            p + 1 => p\n        elif r[1] >= 7:\n            m + 1 => m\n        else:\n            d + 1 => d\n    return [p, m, d]\n\ndef group(score):\n    if score >= 9:\n        return \"promoter\"\n    if score >= 7:\n        return \"passive\"\n    return \"detractor\"\n\ndef total(r):\n    return r[2] + r[3] + r[4]\n\ndef ranked(rows):\n    # by total, highest first; equal totals keep their order (stable)\n    [] => out\n    for r in rows:\n        out + [r] => out\n    for i in [1:len(out) - 1]:\n        i => j\n        while j > 0 and total(out[j]) > total(out[j - 1]):\n            out[j] => t\n            out[j - 1] => out[j]\n            t => out[j - 1]\n            j - 1 => j\n    return out\n\ndef top(rows, n, policy):\n    # strict: exactly n, the first n of the ranking; dense: everyone with a\n    # total at least the n-th; exclude: everyone with a total above the\n    # n-th's when the n-th is tied with the next one, else the first n\n    ranked(rows) => r\n    if n >= len(r):\n        return r\n    total(r[n - 1]) => cut\n    [] => out\n    if policy == \"strict\":\n        for i in [0:n - 1]:\n            out + [r[i]] => out\n        return out\n    if policy == \"dense\":\n        for x in r:\n            if total(x) >= cut:\n                out + [x] => out\n        return out\n    if total(r[n]) != cut:\n        for i in [0:n - 1]:\n            out + [r[i]] => out\n        return out\n    for x in r:\n        if total(x) > cut:\n            out + [x] => out\n    return out\n\ndef reversed_rows(rows):\n    [] => out\n    for r in rows:\n        [r] + out => out\n    return out\n",
      "python": "def gcd(a, b):\n    while b != 0:\n        r = a % b\n        a = b\n        b = r\n    return a\n\ndef fraction_text(num, den):\n    sign = \"\"\n    a = num\n    if num < 0:\n        sign = \"-\"\n        a = 0 - num\n    g = gcd(a, den)\n    if g == 0:\n        g = 1\n    a = int(a / g)\n    d = int(den / g)\n    if d == 1:\n        return sign + str(a)\n    return sign + str(a) + \"/\" + str(d)\n\ndef two_places(num, den):\n    sign = \"\"\n    a = num\n    if num < 0:\n        sign = \"-\"\n        a = 0 - num\n    x = a * 100\n    r = x % den\n    q = int((x - r) / den)\n    if 2 * r >= den:\n        q = q + 1\n    c = q % 100\n    w = int((q - c) / 100)\n    if c < 10:\n        return sign + str(w) + \".0\" + str(c)\n    return sign + str(w) + \".\" + str(c)\n\ndef column(rows, k):\n    out = []\n    for r in rows:\n        out = out + [r[k]]\n    return out\n\ndef sorted_ints(xs):\n    out = []\n    for x in xs:\n        out = out + [x]\n    for i in range(1, len(out)):\n        j = i\n        while j > 0 and out[j] < out[j - 1]:\n            t = out[j]\n            out[j] = out[j - 1]\n            out[j - 1] = t\n            j = j - 1\n    return out\n\ndef median_twice(xs):\n    s = sorted_ints(xs)\n    n = len(s)\n    if n % 2 == 1:\n        return 2 * s[int((n - 1) / 2)]\n    return s[int(n / 2) - 1] + s[int(n / 2)]\n\ndef nps(rows):\n    p = 0\n    m = 0\n    d = 0\n    for r in rows:\n        if r[1] >= 9:\n            p = p + 1\n        elif r[1] >= 7:\n            m = m + 1\n        else:\n            d = d + 1\n    return [p, m, d]\n\ndef group(score):\n    if score >= 9:\n        return \"promoter\"\n    if score >= 7:\n        return \"passive\"\n    return \"detractor\"\n\ndef total(r):\n    return r[2] + r[3] + r[4]\n\ndef ranked(rows):\n    out = []\n    for r in rows:\n        out = out + [r]\n    for i in range(1, len(out)):\n        j = i\n        while j > 0 and total(out[j]) > total(out[j - 1]):\n            t = out[j]\n            out[j] = out[j - 1]\n            out[j - 1] = t\n            j = j - 1\n    return out\n\ndef top(rows, n, policy):\n    r = ranked(rows)\n    if n >= len(r):\n        return r\n    cut = total(r[n - 1])\n    out = []\n    if policy == \"strict\":\n        for i in range(0, n):\n            out = out + [r[i]]\n        return out\n    if policy == \"dense\":\n        for x in r:\n            if total(x) >= cut:\n                out = out + [x]\n        return out\n    if total(r[n]) != cut:\n        for i in range(0, n):\n            out = out + [r[i]]\n        return out\n    for x in r:\n        if total(x) > cut:\n            out = out + [x]\n    return out\n\ndef reversed_rows(rows):\n    out = []\n    for r in rows:\n        out = [r] + out\n    return out\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n4\n\n4\n0\n4\n13\n4\nx\n6\n\n6\nzed 8 4 4\n6\nana 8 4 4 4\n6\nZed 8 4 4 4\n6\nzedzedz 8 4 4 4\n6\nzed 11 4 4 4\n6\nzed 8 6 4 4\n6\nzed x 4 4 4\n7\n",
      "screen": "== Survey analyzer ==\nA survey: how likely each person is to recommend us (0 to 10), and three questions on a scale of 1 to 5.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 0\nPick a number from 1 to 7.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> x\nPick a number from 1 to 7.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 4\nhow many at the top (1 to 12)> \nCancelled.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 4\nhow many at the top (1 to 12)> 0\nType a whole number from 1 to 12.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 4\nhow many at the top (1 to 12)> 13\nType a whole number from 1 to 12.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 4\nhow many at the top (1 to 12)> x\nType a whole number from 1 to 12.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 6\nA response as name, recommend (0 to 10) and q1 to q3 (1 to 5), such as zed 8 4 4 3.\nresponse> \nCancelled.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 6\nA response as name, recommend (0 to 10) and q1 to q3 (1 to 5), such as zed 8 4 4 3.\nresponse> zed 8 4 4\nType a name and four numbers.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 6\nA response as name, recommend (0 to 10) and q1 to q3 (1 to 5), such as zed 8 4 4 3.\nresponse> ana 8 4 4 4\nana has answered already.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 6\nA response as name, recommend (0 to 10) and q1 to q3 (1 to 5), such as zed 8 4 4 3.\nresponse> Zed 8 4 4 4\nA name is lower-case letters.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 6\nA response as name, recommend (0 to 10) and q1 to q3 (1 to 5), such as zed 8 4 4 3.\nresponse> zedzedz 8 4 4 4\nKeep a name to 6 letters.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 6\nA response as name, recommend (0 to 10) and q1 to q3 (1 to 5), such as zed 8 4 4 3.\nresponse> zed 11 4 4 4\nThe recommend score is a whole number from 0 to 10.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 6\nA response as name, recommend (0 to 10) and q1 to q3 (1 to 5), such as zed 8 4 4 3.\nresponse> zed 8 6 4 4\nq1 to q3 are whole numbers from 1 to 5.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 6\nA response as name, recommend (0 to 10) and q1 to q3 (1 to 5), such as zed 8 4 4 3.\nresponse> zed x 4 4 4\nThe recommend score is a whole number from 0 to 10.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\n3\n4\n4\n4\n2\n5\n6\nzed 6 5 5 5\n3\n4\n4\n7\n",
      "screen": "== Survey analyzer ==\nA survey: how likely each person is to recommend us (0 to 10), and three questions on a scale of 1 to 5.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 1\n  name   recommend  group      q1 q2 q3  total\n  ana           10  promoter   5  4  5     14\n  bo             9  promoter   4  4  4     12\n  cy             8  passive    4  3  4     11\n  dee            7  passive    3  4  3     10\n  eli            6  detractor  3  3  2      8\n  fay            9  promoter   5  5  4     14\n  gus            3  detractor  2  1  2      5\n  hal           10  promoter   5  5  5     15\n  ivy            8  passive    4  4  3     11\n  jo             5  detractor  2  3  3      8\n  kai            9  promoter   4  4  4     12\n  lu             7  passive    4  3  4     11\n12 responses.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 2\n  recommend mean 7.58 (91/12), median 8   3:1  5:1  6:1  7:2  8:2  9:3  10:2\n  q1        mean 3.75 (15/4), median 4   2:2  3:2  4:5  5:3\n            answered 4 or 5: 8 of 12, 66.67%\n  q2        mean 3.58 (43/12), median 4   1:1  3:4  4:5  5:2\n            answered 4 or 5: 7 of 12, 58.33%\n  q3        mean 3.58 (43/12), median 4   2:2  3:3  4:5  5:2\n            answered 4 or 5: 7 of 12, 58.33%\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 3\nPromoters (9-10): 5, passives (7-8): 4, detractors (0-6): 3, of 12.\nNet Promoter Score: 16.67 (50/3), on a scale from -100 to +100.\nOne point more would change the score only for cy 8 to 9, eli 6 to 7, ivy 8 to 9: a recommend score counts only when it crosses 6 to 7 or 8 to 9.\n7 others would raise the mean recommend score and leave the Net Promoter Score as it is.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 4\nhow many at the top (1 to 12)> 3\nTop 3 by total of q1 to q3:\n  strict  3: hal 15, ana 14, fay 14\n  dense   3: hal 15, ana 14, fay 14\n  exclude 3: hal 15, ana 14, fay 14\nStrict gives exactly the number asked for, but when the cut falls inside a tie it keeps whoever came first. Dense keeps the whole tie, exclude drops it.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 4\nhow many at the top (1 to 12)> 4\nTop 4 by total of q1 to q3:\n  strict  4: hal 15, ana 14, fay 14, bo 12\n          with the responses in the reverse order: hal 15, fay 14, ana 14, kai 12\n  dense   5: hal 15, ana 14, fay 14, bo 12, kai 12\n  exclude 3: hal 15, ana 14, fay 14\nStrict gives exactly the number asked for, but when the cut falls inside a tie it keeps whoever came first. Dense keeps the whole tie, exclude drops it.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 4\nhow many at the top (1 to 12)> 2\nTop 2 by total of q1 to q3:\n  strict  2: hal 15, ana 14\n          with the responses in the reverse order: hal 15, fay 14\n  dense   3: hal 15, ana 14, fay 14\n  exclude 1: hal 15\nStrict gives exactly the number asked for, but when the cut falls inside a tie it keeps whoever came first. Dense keeps the whole tie, exclude drops it.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 5\n  recommend 7 or more  9, mean total of q1 to q3 12.22\n  recommend 6 or less  3, mean total of q1 to q3 7.00\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 6\nA response as name, recommend (0 to 10) and q1 to q3 (1 to 5), such as zed 8 4 4 3.\nresponse> zed 6 5 5 5\nzed added.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 3\nPromoters (9-10): 5, passives (7-8): 4, detractors (0-6): 4, of 13.\nNet Promoter Score: 7.69 (100/13), on a scale from -100 to +100.\nOne point more would change the score only for cy 8 to 9, eli 6 to 7, ivy 8 to 9, zed 6 to 7: a recommend score counts only when it crosses 6 to 7 or 8 to 9.\n7 others would raise the mean recommend score and leave the Net Promoter Score as it is.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 4\nhow many at the top (1 to 13)> 4\nTop 4 by total of q1 to q3:\n  strict  4: hal 15, zed 15, ana 14, fay 14\n  dense   4: hal 15, zed 15, ana 14, fay 14\n  exclude 4: hal 15, zed 15, ana 14, fay 14\nStrict gives exactly the number asked for, but when the cut falls inside a tie it keeps whoever came first. Dense keeps the whole tie, exclude drops it.\n\n1) responses  2) questions  3) Net Promoter Score  4) top respondents  5) satisfied or not  6) add a response  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "survey-score-analyzer",
      "caseId": "044-survey-score-analyzer",
      "title": "Survey score analyzer"
    },
    {
      "slug": "top-n-with-ties",
      "caseId": "273-top-n-with-ties",
      "title": "Top-N with ties — four people tied for third"
    }
  ],
  "updated": "2026-10-16"
}
