{
  "id": "P104",
  "slug": "money-ledger",
  "key": "P104-money-ledger",
  "title": "Money ledger",
  "summary": "A ledger kept in whole cents beside the same sums added as floats; one amount added many times; tax on every line under four rounding rules and the drift each one leaves over many halves; and an amount shared by weights three ways, two of which always add up.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P104 - Money ledger\n\nA ledger kept in whole cents, next to the same sums added as floats. The\nprogram also adds one amount many times, works out tax on every line\nunder four rounding rules, and shares an amount by weights so that the\nparts add up to it.\n\n- `main.eml` - the menu, the ledger table, the repeated amount, the tax\n  table, sharing and entries\n- `money.eml` - amounts in cents and their text, float and exact totals,\n  the four rounding rules, and the three ways of sharing\n\nHow each part works:\n\n- Money is whole cents. Typed amounts have at most two decimals, and\n  nothing is ever a float except the float column itself.\n- Floats, as in the corpus case `money-float-accumulation`: the same\n  entries added as floats can miss the exact total. Ten additions of 0.01\n  are already off, and the float total comes out low for one count and\n  high for the next. A report with two decimals hides the difference: it\n  prints 0.00, or -0.00.\n- Tax on every line: each line's tax is an exact fraction of a cent,\n  rounded by one of four rules:\n  - half-up: a half away from zero;\n  - half-even: a half to the even cent;\n  - half-down: a half toward zero;\n  - truncate: the fraction dropped.\n\n  The program shows the lines rounded and then added, how far that is from\n  the exact tax, and the total rounded once.\n- The ledger of halves: 21 lines of 0.01, 0.03 ... 0.41. At 50% every\n  line's tax ends in exactly half a cent. As in the corpus case\n  `rounding-rule-drift`, the rules that send every half the same way drift\n  by 10.5 cents, and half-even by half a cent.\n- Sharing by weights, three ways:\n  - every part rounded on its own, which can lose or invent a cent;\n  - the largest remainders: each part rounded down, then the cents left\n    over go to the largest remainders, the earlier part first on a tie;\n  - the running total rounded, each part being the difference.\n\n  The last two always add up to the amount, but can give the leftover\n  cent to different people.\n\nWhat is checked: menu choices 1 to 7; an amount with at most two\ndecimals, up to 9,999,999.99; a description of up to 12 characters, and\n30 entries at most; a tax rate from 0.01% to 100%; 2 to 8 weights, each a\nwhole number from 1 to 9999. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the starting ledger, whose float total is 56.39000000000001 against an\n  exact 56.39;\n- 0.01, 0.10 and 19.99 added 10, 100, 1,000 and 2,000 times;\n- 8.25% tax on the ledger;\n- the ledger of halves at 50%;\n- 100.00 shared 1 1 1, 0.05 shared four ways, and 10.00 shared seven\n  ways, where rounding each part invents a cent;\n- an empty ledger, rent 850.00 and a deposit of -850.00.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- entries: a cancel, a description too long, amounts with three decimals,\n  a comma and too many digits, then a cancel;\n- the repeated amount 0, a cancel and abc;\n- tax rates 0, 100.01 and abc, then a cancel;\n- sharing: 0, -3, one weight, a weight of 0, nine weights, and a cancel;\n- a new ledger of the wrong kind, then a cancel.\n\nBuilt on the verified corpus cases `money-float-accumulation` (floats\nare already off at ten items, on no stable side) and\n`rounding-rule-drift` (a rule that sends every half one way drifts by\nhalf the number of ties).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P104 money ledger: a ledger kept in whole cents beside the same sums in\n# floats, one amount added many times, tax on every line under four\n# rounding rules, and an amount shared by weights.\nimport money\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 ask_amount(prompt):\n    # [cents], [] to cancel, or [\"!\"] after a complaint\n    plain(input(prompt + \"> \")) => t\n    if t == \"\":\n        return []\n    money.parse_amount(t) => a\n    if len(a) == 0:\n        \"Type an amount such as 12.34 or -5, at most two decimals and 9,999,999.99.\" ^0\n        return [\"!\"]\n    return a\n\ndef show_ledger(entries):\n    if len(entries) == 0:\n        \"The ledger is empty.\" ^0\n        return\n    \"  entry          amount     running total\" ^0\n    0 => run\n    for e in entries:\n        run + e[1] => run\n        (\"  \" + pad(e[0], 12) + lpad(money.cents_text(e[1]), 9) + lpad(money.cents_text(run), 18)) ^0\n    money.float_total(entries) => f\n    money.exact_total(entries) => x\n    f - x / 100.0 => d\n    (\"In whole cents: \" + money.cents_text(x) + \". Added as floats: \" + str(f) + \".\") ^0\n    if d == 0.0:\n        \"The two agree this time.\" ^0\n    else:\n        (\"They differ by \" + str(d) + \" - a report with two decimals prints that difference as \" + money.float_cents_text(d) + \".\") ^0\n\ndef many():\n    ask_amount(\"amount\") => a\n    if len(a) == 0:\n        \"Cancelled.\" ^0\n        return\n    if a[0] == \"!\":\n        return\n    if a[0] == 0:\n        \"Pick an amount that is not zero.\" ^0\n        return\n    (\"  times   as floats                exact          equal\") ^0\n    for n in [10, 100, 1000, 2000]:\n        money.repeat(a[0], n) => r\n        \"yes\" => eq\n        if r[0] != r[1] / 100.0:\n            \"no, low\" => eq\n            if r[0] > r[1] / 100.0:\n                \"no, high\" => eq\n        (\"  \" + lpad(str(n), 5) + \"   \" + pad(str(r[0]), 22) + lpad(money.cents_text(r[1]), 13) + \"  \" + eq) ^0\n    \"Ten additions can already be off, and the float total can come out low for one count and high for the next.\" ^0\n\ndef micro(num):\n    # num / 10000 cents, written as an amount with six decimals\n    \"\" => sign\n    num => a\n    if num < 0:\n        \"-\" => sign\n        0 - num => a\n    a % 10000 => f\n    int((a - f) / 10000) => c\n    str(f) => fs\n    while len(fs) < 4:\n        \"0\" + fs => fs\n    money.cents_text(c) => ct\n    return sign + ct + fs\n\ndef tax(entries):\n    if len(entries) == 0:\n        \"The ledger is empty.\" ^0\n        return\n    plain(input(\"tax rate in percent (0.01 to 100)> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    money.parse_amount(t) => r\n    if len(r) == 0 or r[0] < 1 or r[0] > 10000:\n        \"Type a rate from 0.01 to 100, at most two decimals.\" ^0\n        return\n    r[0] => bp\n    0 => ties\n    for e in entries:\n        if money.is_tie(e[1] * bp, 10000):\n            ties + 1 => ties\n    money.exact_total(entries) * bp => exact\n    \" lines land\" => land\n    if ties == 1:\n        \" line lands\" => land\n    (money.cents_text(bp) + \"% on every line: the exact tax is \" + micro(exact) + \"; \" + str(ties) + \" of the \" + str(len(entries)) + land + \" exactly on half a cent.\") ^0\n    \"  rule        lines rounded, then added   off by       the total rounded\" ^0\n    for rule in [\"half-up\", \"half-even\", \"half-down\", \"truncate\"]:\n        0 => s\n        for e in entries:\n            s + money.round_div(e[1] * bp, 10000, rule) => s\n        money.round_div(exact, 10000, rule) => once\n        micro(s * 10000 - exact) => off\n        if s * 10000 - exact > 0:\n            \"+\" + off => off\n        (\"  \" + pad(rule, 12) + lpad(money.cents_text(s), 12) + \"             \" + pad(off, 13) + lpad(money.cents_text(once), 10)) ^0\n    \"half-up sends every half a cent away from zero, half-down toward it, and truncate drops the fraction. half-even goes to the even cent, so over many lines its halves tend to cancel out.\" ^0\n\ndef share_menu():\n    ask_amount(\"amount to share\") => a\n    if len(a) == 0:\n        \"Cancelled.\" ^0\n        return\n    if a[0] == \"!\":\n        return\n    if a[0] <= 0:\n        \"Share an amount above zero.\" ^0\n        return\n    plain(input(\"weights, whole numbers such as 1 1 1 or 50 30 20> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    [] => ws\n    \"\" => w\n    True => ok\n    for c in t + \" \":\n        if c == \" \":\n            if w != \"\":\n                number(w, 4) => v\n                if v < 1:\n                    False => ok\n                ws + [v] => ws\n            \"\" => w\n        else:\n            w + c => w\n    if not ok or len(ws) < 2 or len(ws) > 8:\n        \"Type 2 to 8 whole numbers from 1 to 9999.\" ^0\n        return\n    (\"Sharing \" + money.cents_text(a[0]) + \" by \" + t + \":\") ^0\n    [\"each\", \"largest\", \"running\"] => methods\n    [\"each rounded\", \"largest remainder\", \"running total\"] => names\n    for k in [0:2]:\n        money.share(a[0], ws, methods[k]) => parts\n        \"\" => line\n        0 => sum\n        for p in parts:\n            if line != \"\":\n                line + \"  \" => line\n            line + money.cents_text(p) => line\n            sum + p => sum\n        \"\" => note\n        if sum != a[0]:\n            \"  adds up to \" + money.cents_text(sum) + \", not \" + money.cents_text(a[0]) => note\n        (\"  \" + pad(names[k], 18) + line + note) ^0\n    \"Rounding every part on its own can lose or invent cents. The largest remainders, or the rounded running total, always add up; they can give a leftover cent to different people.\" ^0\n\ndef add(entries):\n    plain(input(\"what (up to 12 letters)> \")) => d\n    if d == \"\":\n        \"Cancelled.\" ^0\n        return entries\n    if len(d) > 12:\n        \"Keep it to 12 characters.\" ^0\n        return entries\n    ask_amount(\"amount\") => a\n    if len(a) == 0:\n        \"Cancelled.\" ^0\n        return entries\n    if a[0] == \"!\":\n        return entries\n    if len(entries) >= 30:\n        \"The ledger holds 30 entries.\" ^0\n        return entries\n    (d + \" \" + money.cents_text(a[0]) + \" added.\") ^0\n    return entries + [[d, a[0]]]\n\n[[\"coffee\", 310], [\"bus\", 270], [\"lunch\", 1260], [\"refund\", 0 - 500], [\"books\", 3999], [\"snack\", 110], [\"tip\", 70], [\"milk\", 120]] => entries\n\n\"== Money ledger ==\" ^0\n\"Money kept in whole cents, next to the same sums added as floats; rounding rules for fractions of a cent; and sharing an amount so the parts add up.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_ledger(entries)\n    elif choice == \"2\":\n        add(entries) => entries\n    elif choice == \"3\":\n        many()\n    elif choice == \"4\":\n        tax(entries)\n    elif choice == \"5\":\n        share_menu()\n    elif choice == \"6\":\n        plain(input(\"empty, or halves (21 lines of 0.01, 0.03 ... 0.41)> \")) => k\n        if k == \"empty\":\n            [] => entries\n            \"The ledger is empty.\" ^0\n        elif k == \"halves\":\n            [] => entries\n            for i in [0:20]:\n                entries + [[\"line \" + str(i + 1), 2 * i + 1]] => entries\n            \"21 lines of odd cents: at 50% each one's tax is exactly half a cent over a whole one.\" ^0\n        elif k == \"\":\n            \"Cancelled.\" ^0\n        else:\n            \"Type empty or halves.\" ^0\n    elif choice == \"7\":\n        False => running\n    else:\n        \"Pick a number from 1 to 7.\" ^0\n\"Bye.\" ^0\n",
      "python": "import money\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 ask_amount(prompt):\n    t = plain(input(prompt + \"> \"))\n    if t == \"\":\n        return []\n    a = money.parse_amount(t)\n    if len(a) == 0:\n        print(\"Type an amount such as 12.34 or -5, at most two decimals and 9,999,999.99.\")\n        return [\"!\"]\n    return a\n\ndef show_ledger(entries):\n    if len(entries) == 0:\n        print(\"The ledger is empty.\")\n        return\n    print(\"  entry          amount     running total\")\n    run = 0\n    for e in entries:\n        run = run + e[1]\n        print(\"  \" + pad(e[0], 12) + lpad(money.cents_text(e[1]), 9) + lpad(money.cents_text(run), 18))\n    f = money.float_total(entries)\n    x = money.exact_total(entries)\n    d = f - x / 100.0\n    print(\"In whole cents: \" + money.cents_text(x) + \". Added as floats: \" + str(f) + \".\")\n    if d == 0.0:\n        print(\"The two agree this time.\")\n    else:\n        print(\"They differ by \" + str(d) + \" - a report with two decimals prints that difference as \" + money.float_cents_text(d) + \".\")\n\ndef many():\n    a = ask_amount(\"amount\")\n    if len(a) == 0:\n        print(\"Cancelled.\")\n        return\n    if a[0] == \"!\":\n        return\n    if a[0] == 0:\n        print(\"Pick an amount that is not zero.\")\n        return\n    print(\"  times   as floats                exact          equal\")\n    for n in [10, 100, 1000, 2000]:\n        r = money.repeat(a[0], n)\n        eq = \"yes\"\n        if r[0] != r[1] / 100.0:\n            eq = \"no, low\"\n            if r[0] > r[1] / 100.0:\n                eq = \"no, high\"\n        print(\"  \" + lpad(str(n), 5) + \"   \" + pad(str(r[0]), 22) + lpad(money.cents_text(r[1]), 13) + \"  \" + eq)\n    print(\"Ten additions can already be off, and the float total can come out low for one count and high for the next.\")\n\ndef micro(num):\n    sign = \"\"\n    a = num\n    if num < 0:\n        sign = \"-\"\n        a = 0 - num\n    f = a % 10000\n    c = int((a - f) / 10000)\n    fs = str(f)\n    while len(fs) < 4:\n        fs = \"0\" + fs\n    ct = money.cents_text(c)\n    return sign + ct + fs\n\ndef tax(entries):\n    if len(entries) == 0:\n        print(\"The ledger is empty.\")\n        return\n    t = plain(input(\"tax rate in percent (0.01 to 100)> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    r = money.parse_amount(t)\n    if len(r) == 0 or r[0] < 1 or r[0] > 10000:\n        print(\"Type a rate from 0.01 to 100, at most two decimals.\")\n        return\n    bp = r[0]\n    ties = 0\n    for e in entries:\n        if money.is_tie(e[1] * bp, 10000):\n            ties = ties + 1\n    exact = money.exact_total(entries) * bp\n    land = \" lines land\"\n    if ties == 1:\n        land = \" line lands\"\n    print(money.cents_text(bp) + \"% on every line: the exact tax is \" + micro(exact) + \"; \" + str(ties) + \" of the \" + str(len(entries)) + land + \" exactly on half a cent.\")\n    print(\"  rule        lines rounded, then added   off by       the total rounded\")\n    for rule in [\"half-up\", \"half-even\", \"half-down\", \"truncate\"]:\n        s = 0\n        for e in entries:\n            s = s + money.round_div(e[1] * bp, 10000, rule)\n        once = money.round_div(exact, 10000, rule)\n        off = micro(s * 10000 - exact)\n        if s * 10000 - exact > 0:\n            off = \"+\" + off\n        print(\"  \" + pad(rule, 12) + lpad(money.cents_text(s), 12) + \"             \" + pad(off, 13) + lpad(money.cents_text(once), 10))\n    print(\"half-up sends every half a cent away from zero, half-down toward it, and truncate drops the fraction. half-even goes to the even cent, so over many lines its halves tend to cancel out.\")\n\ndef share_menu():\n    a = ask_amount(\"amount to share\")\n    if len(a) == 0:\n        print(\"Cancelled.\")\n        return\n    if a[0] == \"!\":\n        return\n    if a[0] <= 0:\n        print(\"Share an amount above zero.\")\n        return\n    t = plain(input(\"weights, whole numbers such as 1 1 1 or 50 30 20> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    ws = []\n    w = \"\"\n    ok = True\n    for c in t + \" \":\n        if c == \" \":\n            if w != \"\":\n                v = number(w, 4)\n                if v < 1:\n                    ok = False\n                ws = ws + [v]\n            w = \"\"\n        else:\n            w = w + c\n    if not ok or len(ws) < 2 or len(ws) > 8:\n        print(\"Type 2 to 8 whole numbers from 1 to 9999.\")\n        return\n    print(\"Sharing \" + money.cents_text(a[0]) + \" by \" + t + \":\")\n    methods = [\"each\", \"largest\", \"running\"]\n    names = [\"each rounded\", \"largest remainder\", \"running total\"]\n    for k in range(0, 3):\n        parts = money.share(a[0], ws, methods[k])\n        line = \"\"\n        sum = 0\n        for p in parts:\n            if line != \"\":\n                line = line + \"  \"\n            line = line + money.cents_text(p)\n            sum = sum + p\n        note = \"\"\n        if sum != a[0]:\n            note = \"  adds up to \" + money.cents_text(sum) + \", not \" + money.cents_text(a[0])\n        print(\"  \" + pad(names[k], 18) + line + note)\n    print(\"Rounding every part on its own can lose or invent cents. The largest remainders, or the rounded running total, always add up; they can give a leftover cent to different people.\")\n\ndef add(entries):\n    d = plain(input(\"what (up to 12 letters)> \"))\n    if d == \"\":\n        print(\"Cancelled.\")\n        return entries\n    if len(d) > 12:\n        print(\"Keep it to 12 characters.\")\n        return entries\n    a = ask_amount(\"amount\")\n    if len(a) == 0:\n        print(\"Cancelled.\")\n        return entries\n    if a[0] == \"!\":\n        return entries\n    if len(entries) >= 30:\n        print(\"The ledger holds 30 entries.\")\n        return entries\n    print(d + \" \" + money.cents_text(a[0]) + \" added.\")\n    return entries + [[d, a[0]]]\n\nentries = [[\"coffee\", 310], [\"bus\", 270], [\"lunch\", 1260], [\"refund\", 0 - 500], [\"books\", 3999], [\"snack\", 110], [\"tip\", 70], [\"milk\", 120]]\nprint(\"== Money ledger ==\")\nprint(\"Money kept in whole cents, next to the same sums added as floats; rounding rules for fractions of a cent; and sharing an amount so the parts add up.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_ledger(entries)\n    elif choice == \"2\":\n        entries = add(entries)\n    elif choice == \"3\":\n        many()\n    elif choice == \"4\":\n        tax(entries)\n    elif choice == \"5\":\n        share_menu()\n    elif choice == \"6\":\n        k = plain(input(\"empty, or halves (21 lines of 0.01, 0.03 ... 0.41)> \"))\n        if k == \"empty\":\n            entries = []\n            print(\"The ledger is empty.\")\n        elif k == \"halves\":\n            entries = []\n            for i in range(0, 21):\n                entries = entries + [[\"line \" + str(i + 1), 2 * i + 1]]\n            print(\"21 lines of odd cents: at 50% each one's tax is exactly half a cent over a whole one.\")\n        elif k == \"\":\n            print(\"Cancelled.\")\n        else:\n            print(\"Type empty or halves.\")\n    elif choice == \"7\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 7.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "money.eml",
      "eml": "# P104 money ledger - money in whole cents, beside the same sums in floats\n# (the corpus case money-float-accumulation: ten items are already off,\n# and the side the error falls on is not stable); fractions of a cent\n# rounded by four rules, whose drift over many ties is the corpus case\n# rounding-rule-drift; and an amount shared by weights so that the parts\n# add up to it exactly.\n\ndef cents_text(c):\n    # 123456 -> \"1,234.56\", -5 -> \"-0.05\"\n    \"\" => sign\n    c => a\n    if c < 0:\n        \"-\" => sign\n        0 - c => a\n    a % 100 => small\n    int((a - small) / 100) => big\n    str(big) => s\n    \"\" => out\n    0 => k\n    for i in [0:len(s) - 1]:\n        s[len(s) - 1 - i] + out => out\n        k + 1 => k\n        if k % 3 == 0 and i < len(s) - 1:\n            \",\" + out => out\n    if small < 10:\n        return sign + out + \".0\" + str(small)\n    return sign + out + \".\" + str(small)\n\ndef parse_amount(t):\n    # \"12.34\", \"-5\", \"0.5\" -> cents, or [] if not a sum of money with at\n    # most two decimals and at most 9,999,999.99\n    if len(t) == 0:\n        return []\n    1 => sign\n    t => s\n    if s[0] == \"-\":\n        0 - 1 => sign\n        s[1:len(s)] => s\n    \"\" => whole\n    \"\" => frac\n    False => dot\n    for c in s:\n        if c == \".\":\n            if dot:\n                return []\n            True => dot\n        elif c >= \"0\" and c <= \"9\":\n            if dot:\n                frac + c => frac\n            else:\n                whole + c => whole\n        else:\n            return []\n    if whole == \"\" or len(frac) > 2 or (dot and frac == \"\") or len(whole) > 7:\n        return []\n    while len(frac) < 2:\n        frac + \"0\" => frac\n    return [sign * (int(whole) * 100 + int(frac))]\n\ndef float_total(entries):\n    0.0 => t\n    for e in entries:\n        t + e[1] / 100.0 => t\n    return t\n\ndef exact_total(entries):\n    0 => t\n    for e in entries:\n        t + e[1] => t\n    return t\n\ndef float_cents_text(x):\n    # how a report with two decimals shows a float: the cents nearest it,\n    # and \"-0.00\" for a tiny negative difference, as Python's %.2f does\n    x * 100.0 => y\n    \"\" => sign\n    if y < 0:\n        \"-\" => sign\n        0.0 - y => y\n    int(y + 0.5) => c\n    if sign == \"-\" and c == 0:\n        return \"-0.00\"\n    if sign == \"-\":\n        return cents_text(0 - c)\n    return cents_text(c)\n\ndef repeat(amount, n):\n    # [float total, exact cents] of n times the same amount\n    0.0 => t\n    for i in [1:n]:\n        t + amount / 100.0 => t\n    return [t, amount * n]\n\ndef round_div(num, den, rule):\n    # num / den (den > 0) to a whole number by rule: \"half-up\" (half away\n    # from zero), \"half-even\", \"half-down\" (half toward zero), \"truncate\"\n    0 => neg\n    num => a\n    if num < 0:\n        1 => neg\n        0 - num => a\n    a % den => r\n    int((a - r) / den) => q\n    if rule == \"truncate\":\n        0 => up\n    elif 2 * r > den:\n        1 => up\n    elif 2 * r < den:\n        0 => up\n    elif rule == \"half-up\":\n        1 => up\n    elif rule == \"half-down\":\n        0 => up\n    else:\n        q % 2 => up\n    q + up => q\n    if neg == 1:\n        return 0 - q\n    return q\n\ndef is_tie(num, den):\n    0 => a\n    num => a\n    if num < 0:\n        0 - num => a\n    return 2 * (a % den) == den\n\ndef share(total, weights, method):\n    # parts that add up to total, by weight; \"each\" rounds every part on\n    # its own (half-up) and may not add up; \"largest\" gives the cents left\n    # over to the largest remainders; \"running\" rounds the running sum\n    0 => w\n    for x in weights:\n        w + x => w\n    [] => out\n    if method == \"each\":\n        for x in weights:\n            out + [round_div(total * x, w, \"half-up\")] => out\n        return out\n    if method == \"running\":\n        0 => run\n        0 => before\n        for x in weights:\n            run + x => run\n            round_div(total * run, w, \"half-up\") => now\n            out + [now - before] => out\n            now => before\n        return out\n    # largest remainder: floors first, then one cent each to the largest\n    # remainders, earlier first on ties\n    [] => rems\n    0 => given\n    for x in weights:\n        total * x => num\n        num % w => r\n        int((num - r) / w) => f\n        out + [f] => out\n        rems + [r] => rems\n        given + f => given\n    total - given => left\n    [] => used\n    for x in weights:\n        used + [False] => used\n    0 => k\n    while k < left:\n        -1 => best\n        for i in [0:len(weights) - 1]:\n            if not used[i] and (best == -1 or rems[i] > rems[best]):\n                i => best\n        True => used[best]\n        out[best] + 1 => out[best]\n        k + 1 => k\n    return out\n",
      "python": "def cents_text(c):\n    sign = \"\"\n    a = c\n    if c < 0:\n        sign = \"-\"\n        a = 0 - c\n    small = a % 100\n    big = int((a - small) / 100)\n    s = str(big)\n    out = \"\"\n    k = 0\n    for i in range(0, len(s)):\n        out = s[len(s) - 1 - i] + out\n        k = k + 1\n        if k % 3 == 0 and i < len(s) - 1:\n            out = \",\" + out\n    if small < 10:\n        return sign + out + \".0\" + str(small)\n    return sign + out + \".\" + str(small)\n\ndef parse_amount(t):\n    if len(t) == 0:\n        return []\n    sign = 1\n    s = t\n    if s[0] == \"-\":\n        sign = 0 - 1\n        s = s[1:len(s)]\n    whole = \"\"\n    frac = \"\"\n    dot = False\n    for c in s:\n        if c == \".\":\n            if dot:\n                return []\n            dot = True\n        elif c >= \"0\" and c <= \"9\":\n            if dot:\n                frac = frac + c\n            else:\n                whole = whole + c\n        else:\n            return []\n    if whole == \"\" or len(frac) > 2 or dot and frac == \"\" or len(whole) > 7:\n        return []\n    while len(frac) < 2:\n        frac = frac + \"0\"\n    return [sign * (int(whole) * 100 + int(frac))]\n\ndef float_total(entries):\n    t = 0.0\n    for e in entries:\n        t = t + e[1] / 100.0\n    return t\n\ndef exact_total(entries):\n    t = 0\n    for e in entries:\n        t = t + e[1]\n    return t\n\ndef float_cents_text(x):\n    y = x * 100.0\n    sign = \"\"\n    if y < 0:\n        sign = \"-\"\n        y = 0.0 - y\n    c = int(y + 0.5)\n    if sign == \"-\" and c == 0:\n        return \"-0.00\"\n    if sign == \"-\":\n        return cents_text(0 - c)\n    return cents_text(c)\n\ndef repeat(amount, n):\n    t = 0.0\n    for i in range(1, n+1):\n        t = t + amount / 100.0\n    return [t, amount * n]\n\ndef round_div(num, den, rule):\n    neg = 0\n    a = num\n    if num < 0:\n        neg = 1\n        a = 0 - num\n    r = a % den\n    q = int((a - r) / den)\n    if rule == \"truncate\":\n        up = 0\n    elif 2 * r > den:\n        up = 1\n    elif 2 * r < den:\n        up = 0\n    elif rule == \"half-up\":\n        up = 1\n    elif rule == \"half-down\":\n        up = 0\n    else:\n        up = q % 2\n    q = q + up\n    if neg == 1:\n        return 0 - q\n    return q\n\ndef is_tie(num, den):\n    a = 0\n    a = num\n    if num < 0:\n        a = 0 - num\n    return 2 * (a % den) == den\n\ndef share(total, weights, method):\n    w = 0\n    for x in weights:\n        w = w + x\n    out = []\n    if method == \"each\":\n        for x in weights:\n            out = out + [round_div(total * x, w, \"half-up\")]\n        return out\n    if method == \"running\":\n        run = 0\n        before = 0\n        for x in weights:\n            run = run + x\n            now = round_div(total * run, w, \"half-up\")\n            out = out + [now - before]\n            before = now\n        return out\n    rems = []\n    given = 0\n    for x in weights:\n        num = total * x\n        r = num % w\n        f = int((num - r) / w)\n        out = out + [f]\n        rems = rems + [r]\n        given = given + f\n    left = total - given\n    used = []\n    for x in weights:\n        used = used + [False]\n    k = 0\n    while k < left:\n        best = -1\n        for i in range(0, len(weights)):\n            if not used[i] and (best == -1 or rems[i] > rems[best]):\n                best = i\n        used[best] = True\n        out[best] = out[best] + 1\n        k = k + 1\n    return out\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n2\n\n2\na very long description\n2\ntea\n12.345\n2\ntea\n1,50\n2\ntea\n10000000\n2\ntea\n\n3\n0\n3\n\n3\nabc\n4\n0\n4\n100.01\n4\nabc\n4\n\n5\n0\n5\n-3\n5\n10\n1\n5\n10\n1 0 1\n5\n10\n1 2 3 4 5 6 7 8 9\n5\n10\n\n6\nfull\n6\n\n7\n",
      "screen": "== Money ledger ==\nMoney kept in whole cents, next to the same sums added as floats; rounding rules for fractions of a cent; and sharing an amount so the parts add up.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 0\nPick a number from 1 to 7.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> x\nPick a number from 1 to 7.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 2\nwhat (up to 12 letters)> \nCancelled.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 2\nwhat (up to 12 letters)> a very long description\nKeep it to 12 characters.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 2\nwhat (up to 12 letters)> tea\namount> 12.345\nType an amount such as 12.34 or -5, at most two decimals and 9,999,999.99.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 2\nwhat (up to 12 letters)> tea\namount> 1,50\nType an amount such as 12.34 or -5, at most two decimals and 9,999,999.99.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 2\nwhat (up to 12 letters)> tea\namount> 10000000\nType an amount such as 12.34 or -5, at most two decimals and 9,999,999.99.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 2\nwhat (up to 12 letters)> tea\namount> \nCancelled.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 3\namount> 0\nPick an amount that is not zero.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 3\namount> \nCancelled.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 3\namount> abc\nType an amount such as 12.34 or -5, at most two decimals and 9,999,999.99.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 4\ntax rate in percent (0.01 to 100)> 0\nType a rate from 0.01 to 100, at most two decimals.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 4\ntax rate in percent (0.01 to 100)> 100.01\nType a rate from 0.01 to 100, at most two decimals.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 4\ntax rate in percent (0.01 to 100)> abc\nType a rate from 0.01 to 100, at most two decimals.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 4\ntax rate in percent (0.01 to 100)> \nCancelled.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 5\namount to share> 0\nShare an amount above zero.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 5\namount to share> -3\nShare an amount above zero.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 5\namount to share> 10\nweights, whole numbers such as 1 1 1 or 50 30 20> 1\nType 2 to 8 whole numbers from 1 to 9999.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 5\namount to share> 10\nweights, whole numbers such as 1 1 1 or 50 30 20> 1 0 1\nType 2 to 8 whole numbers from 1 to 9999.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 5\namount to share> 10\nweights, whole numbers such as 1 1 1 or 50 30 20> 1 2 3 4 5 6 7 8 9\nType 2 to 8 whole numbers from 1 to 9999.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 5\namount to share> 10\nweights, whole numbers such as 1 1 1 or 50 30 20> \nCancelled.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 6\nempty, or halves (21 lines of 0.01, 0.03 ... 0.41)> full\nType empty or halves.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 6\nempty, or halves (21 lines of 0.01, 0.03 ... 0.41)> \nCancelled.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n3\n0.01\n3\n0.10\n3\n19.99\n4\n8.25\n6\nhalves\n4\n50\n5\n100\n1 1 1\n5\n0.05\n1 1 1 1\n5\n10\n1 1 1 1 1 1 1\n6\nempty\n2\nrent\n850\n2\ndeposit\n-850\n1\n7\n",
      "screen": "== Money ledger ==\nMoney kept in whole cents, next to the same sums added as floats; rounding rules for fractions of a cent; and sharing an amount so the parts add up.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 1\n  entry          amount     running total\n  coffee           3.10              3.10\n  bus              2.70              5.80\n  lunch           12.60             18.40\n  refund          -5.00             13.40\n  books           39.99             53.39\n  snack            1.10             54.49\n  tip              0.70             55.19\n  milk             1.20             56.39\nIn whole cents: 56.39. Added as floats: 56.39000000000001.\nThey differ by 7.105427357601002e-15 - a report with two decimals prints that difference as 0.00.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 3\namount> 0.01\n  times   as floats                exact          equal\n     10   0.09999999999999999            0.10  no, low\n    100   1.0000000000000007             1.00  no, high\n   1000   9.999999999999831             10.00  no, low\n   2000   20.000000000000327            20.00  no, high\nTen additions can already be off, and the float total can come out low for one count and high for the next.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 3\namount> 0.10\n  times   as floats                exact          equal\n     10   0.9999999999999999             1.00  no, low\n    100   9.99999999999998              10.00  no, low\n   1000   99.9999999999986             100.00  no, low\n   2000   199.99999999999292           200.00  no, low\nTen additions can already be off, and the float total can come out low for one count and high for the next.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 3\namount> 19.99\n  times   as floats                exact          equal\n     10   199.9                        199.90  yes\n    100   1999.0000000000007         1,999.00  no, high\n   1000   19990.000000000135        19,990.00  no, high\n   2000   39980.00000000042         39,980.00  no, high\nTen additions can already be off, and the float total can come out low for one count and high for the next.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 4\ntax rate in percent (0.01 to 100)> 8.25\n8.25% on every line: the exact tax is 4.652175; 0 of the 8 lines land exactly on half a cent.\n  rule        lines rounded, then added   off by       the total rounded\n  half-up             4.66             +0.007825          4.65\n  half-even           4.66             +0.007825          4.65\n  half-down           4.66             +0.007825          4.65\n  truncate            4.61             -0.042175          4.65\nhalf-up sends every half a cent away from zero, half-down toward it, and truncate drops the fraction. half-even goes to the even cent, so over many lines its halves tend to cancel out.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 6\nempty, or halves (21 lines of 0.01, 0.03 ... 0.41)> halves\n21 lines of odd cents: at 50% each one's tax is exactly half a cent over a whole one.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 4\ntax rate in percent (0.01 to 100)> 50\n50.00% on every line: the exact tax is 2.205000; 21 of the 21 lines land exactly on half a cent.\n  rule        lines rounded, then added   off by       the total rounded\n  half-up             2.31             +0.105000          2.21\n  half-even           2.20             -0.005000          2.20\n  half-down           2.10             -0.105000          2.20\n  truncate            2.10             -0.105000          2.20\nhalf-up sends every half a cent away from zero, half-down toward it, and truncate drops the fraction. half-even goes to the even cent, so over many lines its halves tend to cancel out.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 5\namount to share> 100\nweights, whole numbers such as 1 1 1 or 50 30 20> 1 1 1\nSharing 100.00 by 1 1 1:\n  each rounded      33.33  33.33  33.33  adds up to 99.99, not 100.00\n  largest remainder 33.34  33.33  33.33\n  running total     33.33  33.34  33.33\nRounding every part on its own can lose or invent cents. The largest remainders, or the rounded running total, always add up; they can give a leftover cent to different people.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 5\namount to share> 0.05\nweights, whole numbers such as 1 1 1 or 50 30 20> 1 1 1 1\nSharing 0.05 by 1 1 1 1:\n  each rounded      0.01  0.01  0.01  0.01  adds up to 0.04, not 0.05\n  largest remainder 0.02  0.01  0.01  0.01\n  running total     0.01  0.02  0.01  0.01\nRounding every part on its own can lose or invent cents. The largest remainders, or the rounded running total, always add up; they can give a leftover cent to different people.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 5\namount to share> 10\nweights, whole numbers such as 1 1 1 or 50 30 20> 1 1 1 1 1 1 1\nSharing 10.00 by 1 1 1 1 1 1 1:\n  each rounded      1.43  1.43  1.43  1.43  1.43  1.43  1.43  adds up to 10.01, not 10.00\n  largest remainder 1.43  1.43  1.43  1.43  1.43  1.43  1.42\n  running total     1.43  1.43  1.43  1.42  1.43  1.43  1.43\nRounding every part on its own can lose or invent cents. The largest remainders, or the rounded running total, always add up; they can give a leftover cent to different people.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 6\nempty, or halves (21 lines of 0.01, 0.03 ... 0.41)> empty\nThe ledger is empty.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 2\nwhat (up to 12 letters)> rent\namount> 850\nrent 850.00 added.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 2\nwhat (up to 12 letters)> deposit\namount> -850\ndeposit -850.00 added.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 1\n  entry          amount     running total\n  rent           850.00            850.00\n  deposit       -850.00              0.00\nIn whole cents: 0.00. Added as floats: 0.0.\nThe two agree this time.\n\n1) ledger  2) add an entry  3) one amount many times  4) tax on every line  5) share an amount  6) new ledger  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "money-float-accumulation",
      "caseId": "264-money-float-accumulation",
      "title": "Money in floats — a difference that prints as -0.00"
    },
    {
      "slug": "rounding-rule-drift",
      "caseId": "268-rounding-rule-drift",
      "title": "Rounding rule drift — why banks use the rule that looks arbitrary"
    }
  ],
  "updated": "2026-10-16"
}
