{
  "id": "P105",
  "slug": "percent-calculator",
  "key": "P105-percent-calculator",
  "title": "Percent calculator",
  "summary": "Percentage points against percent; a price up and back down; a stack of percent and flat steps applied in every order, rounded at each step and exactly; and every whole-cent price that comes to a given one after a percent change - all in cents and hundredths of a percent.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P105 - Percent calculator\n\nPercentage points against percent, a change and its undoing, a stack of\ndiscounts in every order, and a price worked back from what it became.\nEverything is in whole cents and hundredths of a percent, so no float is\ninvolved.\n\n- `main.eml` - the menu and the four calculations\n- `percent.eml` - rounding to a whole number, reading amounts and steps,\n  the steps applied with and without rounding, every order of the steps,\n  and the range of prices that end at a given one\n\nHow each part works:\n\n- Two rates, as in the corpus case `percentage-points-vs-percent`: 4% to\n  6% is +2 percentage points and +50%. Going back, it is -2 points, but\n  about -33.33%. Points undo themselves; relative changes do not.\n- Up and down: a price up p% and then down p%, or down and then up, is\n  multiplied by (1 + p)(1 - p) = 1 - p², so either way it loses p² before\n  any rounding.\n- A stack of steps, each a percent or a flat amount, applied in every\n  order: each step is rounded to the cent (half away from zero), and\n  beside it is the exact result. As in the corpus case\n  `successive-percentage-order`:\n  - flat amounts give the same total in any order;\n  - percentages alone give the same exact result in any order, but\n    rounding at every step can tell orders apart (6.67 against 6.66 on\n    12.34 with -10%, -20% and -25%);\n  - with a flat amount among percentages, the order matters even before\n    rounding.\n- Working a price back: every whole-cent price that comes to the given one\n  after a percent change, rounded to the cent. It can be one price, a\n  range (100 prices end at 1.00 after -99%), or none (no price ends at 1.00\n  after +200%, since three times a whole number of cents jumps over it).\n\nWhat is checked: menu choices 1 to 5; rates from 0% to 1000%; prices\nfrom 0.01 to 9,999,999.99; a percent from 0.01% to 99.99% for up and\ndown; 1 to 4 steps, each a percent above -100% or an amount, at most two\ndecimals. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- 4% to 6% and 6% to 4%, and a first rate of 0;\n- 100.00 and 20%, then 12.34 and 33.33%, up and down;\n- 12.34 with -10%, -20% and -25%; with -10%, -20% and -5.00; and 50.00\n  with -5.00 and -2.50;\n- prices worked back from 80.00 after -20%, 1.00 after -99%, 1.00 after\n  +200%, and 108.25 after +8.25%.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- rates abc, a second-rate cancel, 1001, and a cancel;\n- up and down with a price of 0, percents of 0 and 100, and a cancel;\n- stacks of five steps, of -100%, and with a word, then two cancels;\n- working back with a flat amount and with -120%, then two cancels.\n\nBuilt on the verified corpus cases `percentage-points-vs-percent`\n(points add and undo; relative changes multiply and do not) and\n`successive-percentage-order` (flats commute, percentages only until\nrounding, a mixture not at all).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P105 percent calculator: percentage points against percent, a change up\n# and back down, a stack of discounts in every order, and a price worked\n# back from what it became.\nimport percent\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 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_fixed(prompt, most, low, high, what):\n    # [hundredths], [] to cancel, [\"!\"] after a complaint\n    plain(input(prompt + \"> \")) => t\n    if t == \"\":\n        return []\n    percent.parse_fixed(t, most) => v\n    if len(v) == 0 or v[0] < low or v[0] > high:\n        (\"Type \" + what + \".\") ^0\n        return [\"!\"]\n    return v\n\ndef rates():\n    ask_fixed(\"first rate in percent\", 3, 0, 100000, \"a rate from 0 to 1000, at most two decimals\") => a\n    if len(a) == 0:\n        \"Cancelled.\" ^0\n        return\n    if a[0] == \"!\":\n        return\n    ask_fixed(\"second rate in percent\", 3, 0, 100000, \"a rate from 0 to 1000, at most two decimals\") => b\n    if len(b) == 0:\n        \"Cancelled.\" ^0\n        return\n    if b[0] == \"!\":\n        return\n    a[0] => x\n    b[0] => y\n    \"+\" => sign\n    if y < x:\n        \"\" => sign\n    (\"From \" + percent.hundredths(x) + \"% to \" + percent.hundredths(y) + \"%: \" + sign + percent.hundredths(y - x) + \" percentage points.\") ^0\n    if x == 0:\n        \"As a relative change there is none to give: the first rate is zero.\" ^0\n        return\n    percent.ratio_text((y - x) * 10000, x) => r\n    \"about \" => how\n    if r[1]:\n        \"\" => how\n    (\"Relative to \" + percent.hundredths(x) + \"%, that is \" + how + sign + r[0] + \"%.\") ^0\n    if y > 0:\n        percent.ratio_text((x - y) * 10000, y) => back\n        \"about \" => how2\n        if back[1]:\n            \"\" => how2\n        \"+\" => sign2\n        if x < y:\n            \"\" => sign2\n        (\"Back to \" + percent.hundredths(x) + \"% is \" + sign2 + percent.hundredths(x - y) + \" points, but \" + how2 + sign2 + back[0] + \"% relative to \" + percent.hundredths(y) + \"%: points undo themselves, relative changes do not.\") ^0\n\ndef up_down():\n    ask_fixed(\"price\", 7, 1, 999999999, \"a price from 0.01 to 9,999,999.99\") => p\n    if len(p) == 0:\n        \"Cancelled.\" ^0\n        return\n    if p[0] == \"!\":\n        return\n    ask_fixed(\"percent\", 3, 1, 9999, \"a percent from 0.01 to 99.99\") => q\n    if len(q) == 0:\n        \"Cancelled.\" ^0\n        return\n    if q[0] == \"!\":\n        return\n    p[0] => c\n    q[0] => bp\n    percent.round_div(c * (10000 + bp), 10000) => up\n    percent.round_div(up * (10000 - bp), 10000) => back\n    percent.round_div(c * (10000 - bp), 10000) => down\n    percent.round_div(down * (10000 + bp), 10000) => again\n    percent.hundredths(bp) + \"%\" => pc\n    (\"  \" + percent.hundredths(c) + \" up \" + pc + \" is \" + percent.hundredths(up) + \", then down \" + pc + \" is \" + percent.hundredths(back) + \".\") ^0\n    (\"  \" + percent.hundredths(c) + \" down \" + pc + \" is \" + percent.hundredths(down) + \", then up \" + pc + \" is \" + percent.hundredths(again) + \".\") ^0\n    percent.ratio_text(bp * bp, 10000) => sq\n    \"about \" => how\n    if sq[1]:\n        \"\" => how\n    (\"Either way the two steps multiply the price by (1 + p)(1 - p) = 1 - p squared, losing \" + how + sq[0] + \"% of it before any rounding.\") ^0\n\ndef stack():\n    ask_fixed(\"price\", 7, 1, 999999999, \"a price from 0.01 to 9,999,999.99\") => p\n    if len(p) == 0:\n        \"Cancelled.\" ^0\n        return\n    if p[0] == \"!\":\n        return\n    plain(input(\"1 to 4 steps, such as -10% -20% -25% or -5.00> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    [] => steps\n    \"\" => w\n    True => ok\n    for c in t + \" \":\n        if c == \" \":\n            if w != \"\":\n                percent.parse_step(w) => st\n                if len(st) == 0:\n                    False => ok\n                else:\n                    steps + [st] => steps\n            \"\" => w\n        else:\n            w + c => w\n    if not ok or len(steps) < 1 or len(steps) > 4:\n        \"Type 1 to 4 steps: a percent such as -10% or +8.25% (above -100%), or an amount such as -5.00.\" ^0\n        return\n    p[0] => c\n    [] => seen_r\n    [] => seen_x\n    \"  order\" => head\n    (\"  order\" + \"                              \" + \"rounded each step   exact\") ^0\n    for o in percent.orders(len(steps)):\n        [] => seq\n        \"\" => label\n        for i in o:\n            seq + [steps[i]] => seq\n            if label != \"\":\n                label + \" \" => label\n            label + percent.step_text(steps[i]) => label\n        percent.apply_rounded(c, seq) => r\n        percent.apply_exact(c, seq) => x\n        True => new_r\n        for v in seen_r:\n            if v == r:\n                False => new_r\n        if new_r:\n            seen_r + [r] => seen_r\n        True => new_x\n        for v in seen_x:\n            if percent.same_fraction(v, x):\n                False => new_x\n        if new_x:\n            seen_x + [x] => seen_x\n        percent.ratio_text(x[0], x[1]) => xt\n        \"\" => mark\n        if not xt[1]:\n            \" (about)\" => mark\n        (\"  \" + pad(label, 34) + lpad(percent.hundredths(r), 14) + \"   \" + xt[0] + mark) ^0\n    (str(len(seen_r)) + \" different result\" + plural(len(seen_r)) + \" rounded at each step, \" + str(len(seen_x)) + \" exact.\") ^0\n    True => all_pct\n    True => all_flat\n    for st in steps:\n        if st[0] == \"%\":\n            False => all_flat\n        else:\n            False => all_pct\n    if all_flat:\n        \"Flat amounts give the same total in any order.\" ^0\n    elif all_pct:\n        \"Percentages alone give the same exact result in any order; rounding at every step is what can tell the orders apart.\" ^0\n    else:\n        \"With a flat amount among percentages the order matters even before rounding: a percentage taken after the flat amount is a percentage of less.\" ^0\n\ndef plural(n):\n    if n == 1:\n        return \"\"\n    return \"s\"\n\ndef reverse():\n    ask_fixed(\"price now\", 7, 1, 999999999, \"a price from 0.01 to 9,999,999.99\") => p\n    if len(p) == 0:\n        \"Cancelled.\" ^0\n        return\n    if p[0] == \"!\":\n        return\n    plain(input(\"the change it went through, such as -20% or +8.25%> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    percent.parse_step(t) => st\n    if len(st) == 0 or st[0] != \"%\":\n        \"Type a percent above -100%, such as -20% or +8.25%.\" ^0\n        return\n    percent.originals(p[0], st[1]) => r\n    percent.step_text(st) => s\n    if r[0] > r[1]:\n        (\"No price in whole cents comes to \" + percent.hundredths(p[0]) + \" after \" + s + \" rounded to the cent.\") ^0\n    elif r[0] == r[1]:\n        (\"Only \" + percent.hundredths(r[0]) + \" comes to \" + percent.hundredths(p[0]) + \" after \" + s + \".\") ^0\n    else:\n        (str(r[1] - r[0] + 1) + \" prices, \" + percent.hundredths(r[0]) + \" to \" + percent.hundredths(r[1]) + \", all come to \" + percent.hundredths(p[0]) + \" after \" + s + \" rounded to the cent.\") ^0\n\n\"== Percent calculator ==\" ^0\n\"Percentage points against percent, a change and its undoing, discounts in every order, and a price worked back - all in whole cents and hundredths of a percent.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        rates()\n    elif choice == \"2\":\n        up_down()\n    elif choice == \"3\":\n        stack()\n    elif choice == \"4\":\n        reverse()\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import percent\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 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_fixed(prompt, most, low, high, what):\n    t = plain(input(prompt + \"> \"))\n    if t == \"\":\n        return []\n    v = percent.parse_fixed(t, most)\n    if len(v) == 0 or v[0] < low or v[0] > high:\n        print(\"Type \" + what + \".\")\n        return [\"!\"]\n    return v\n\ndef rates():\n    a = ask_fixed(\"first rate in percent\", 3, 0, 100000, \"a rate from 0 to 1000, at most two decimals\")\n    if len(a) == 0:\n        print(\"Cancelled.\")\n        return\n    if a[0] == \"!\":\n        return\n    b = ask_fixed(\"second rate in percent\", 3, 0, 100000, \"a rate from 0 to 1000, at most two decimals\")\n    if len(b) == 0:\n        print(\"Cancelled.\")\n        return\n    if b[0] == \"!\":\n        return\n    x = a[0]\n    y = b[0]\n    sign = \"+\"\n    if y < x:\n        sign = \"\"\n    print(\"From \" + percent.hundredths(x) + \"% to \" + percent.hundredths(y) + \"%: \" + sign + percent.hundredths(y - x) + \" percentage points.\")\n    if x == 0:\n        print(\"As a relative change there is none to give: the first rate is zero.\")\n        return\n    r = percent.ratio_text((y - x) * 10000, x)\n    how = \"about \"\n    if r[1]:\n        how = \"\"\n    print(\"Relative to \" + percent.hundredths(x) + \"%, that is \" + how + sign + r[0] + \"%.\")\n    if y > 0:\n        back = percent.ratio_text((x - y) * 10000, y)\n        how2 = \"about \"\n        if back[1]:\n            how2 = \"\"\n        sign2 = \"+\"\n        if x < y:\n            sign2 = \"\"\n        print(\"Back to \" + percent.hundredths(x) + \"% is \" + sign2 + percent.hundredths(x - y) + \" points, but \" + how2 + sign2 + back[0] + \"% relative to \" + percent.hundredths(y) + \"%: points undo themselves, relative changes do not.\")\n\ndef up_down():\n    p = ask_fixed(\"price\", 7, 1, 999999999, \"a price from 0.01 to 9,999,999.99\")\n    if len(p) == 0:\n        print(\"Cancelled.\")\n        return\n    if p[0] == \"!\":\n        return\n    q = ask_fixed(\"percent\", 3, 1, 9999, \"a percent from 0.01 to 99.99\")\n    if len(q) == 0:\n        print(\"Cancelled.\")\n        return\n    if q[0] == \"!\":\n        return\n    c = p[0]\n    bp = q[0]\n    up = percent.round_div(c * (10000 + bp), 10000)\n    back = percent.round_div(up * (10000 - bp), 10000)\n    down = percent.round_div(c * (10000 - bp), 10000)\n    again = percent.round_div(down * (10000 + bp), 10000)\n    pc = percent.hundredths(bp) + \"%\"\n    print(\"  \" + percent.hundredths(c) + \" up \" + pc + \" is \" + percent.hundredths(up) + \", then down \" + pc + \" is \" + percent.hundredths(back) + \".\")\n    print(\"  \" + percent.hundredths(c) + \" down \" + pc + \" is \" + percent.hundredths(down) + \", then up \" + pc + \" is \" + percent.hundredths(again) + \".\")\n    sq = percent.ratio_text(bp * bp, 10000)\n    how = \"about \"\n    if sq[1]:\n        how = \"\"\n    print(\"Either way the two steps multiply the price by (1 + p)(1 - p) = 1 - p squared, losing \" + how + sq[0] + \"% of it before any rounding.\")\n\ndef stack():\n    p = ask_fixed(\"price\", 7, 1, 999999999, \"a price from 0.01 to 9,999,999.99\")\n    if len(p) == 0:\n        print(\"Cancelled.\")\n        return\n    if p[0] == \"!\":\n        return\n    t = plain(input(\"1 to 4 steps, such as -10% -20% -25% or -5.00> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    steps = []\n    w = \"\"\n    ok = True\n    for c in t + \" \":\n        if c == \" \":\n            if w != \"\":\n                st = percent.parse_step(w)\n                if len(st) == 0:\n                    ok = False\n                else:\n                    steps = steps + [st]\n            w = \"\"\n        else:\n            w = w + c\n    if not ok or len(steps) < 1 or len(steps) > 4:\n        print(\"Type 1 to 4 steps: a percent such as -10% or +8.25% (above -100%), or an amount such as -5.00.\")\n        return\n    c = p[0]\n    seen_r = []\n    seen_x = []\n    head = \"  order\"\n    print(\"  order\" + \"                              \" + \"rounded each step   exact\")\n    for o in percent.orders(len(steps)):\n        seq = []\n        label = \"\"\n        for i in o:\n            seq = seq + [steps[i]]\n            if label != \"\":\n                label = label + \" \"\n            label = label + percent.step_text(steps[i])\n        r = percent.apply_rounded(c, seq)\n        x = percent.apply_exact(c, seq)\n        new_r = True\n        for v in seen_r:\n            if v == r:\n                new_r = False\n        if new_r:\n            seen_r = seen_r + [r]\n        new_x = True\n        for v in seen_x:\n            if percent.same_fraction(v, x):\n                new_x = False\n        if new_x:\n            seen_x = seen_x + [x]\n        xt = percent.ratio_text(x[0], x[1])\n        mark = \"\"\n        if not xt[1]:\n            mark = \" (about)\"\n        print(\"  \" + pad(label, 34) + lpad(percent.hundredths(r), 14) + \"   \" + xt[0] + mark)\n    print(str(len(seen_r)) + \" different result\" + plural(len(seen_r)) + \" rounded at each step, \" + str(len(seen_x)) + \" exact.\")\n    all_pct = True\n    all_flat = True\n    for st in steps:\n        if st[0] == \"%\":\n            all_flat = False\n        else:\n            all_pct = False\n    if all_flat:\n        print(\"Flat amounts give the same total in any order.\")\n    elif all_pct:\n        print(\"Percentages alone give the same exact result in any order; rounding at every step is what can tell the orders apart.\")\n    else:\n        print(\"With a flat amount among percentages the order matters even before rounding: a percentage taken after the flat amount is a percentage of less.\")\n\ndef plural(n):\n    if n == 1:\n        return \"\"\n    return \"s\"\n\ndef reverse():\n    p = ask_fixed(\"price now\", 7, 1, 999999999, \"a price from 0.01 to 9,999,999.99\")\n    if len(p) == 0:\n        print(\"Cancelled.\")\n        return\n    if p[0] == \"!\":\n        return\n    t = plain(input(\"the change it went through, such as -20% or +8.25%> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    st = percent.parse_step(t)\n    if len(st) == 0 or st[0] != \"%\":\n        print(\"Type a percent above -100%, such as -20% or +8.25%.\")\n        return\n    r = percent.originals(p[0], st[1])\n    s = percent.step_text(st)\n    if r[0] > r[1]:\n        print(\"No price in whole cents comes to \" + percent.hundredths(p[0]) + \" after \" + s + \" rounded to the cent.\")\n    elif r[0] == r[1]:\n        print(\"Only \" + percent.hundredths(r[0]) + \" comes to \" + percent.hundredths(p[0]) + \" after \" + s + \".\")\n    else:\n        print(str(r[1] - r[0] + 1) + \" prices, \" + percent.hundredths(r[0]) + \" to \" + percent.hundredths(r[1]) + \", all come to \" + percent.hundredths(p[0]) + \" after \" + s + \" rounded to the cent.\")\n\nprint(\"== Percent calculator ==\")\nprint(\"Percentage points against percent, a change and its undoing, discounts in every order, and a price worked back - all in whole cents and hundredths of a percent.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        rates()\n    elif choice == \"2\":\n        up_down()\n    elif choice == \"3\":\n        stack()\n    elif choice == \"4\":\n        reverse()\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "percent.eml",
      "eml": "# P105 percent calculator - money in whole cents, percentages in basis\n# points (hundredths of a percent), so nothing passes through a float.\n# Percentage points add and undo; relative changes multiply and do not\n# (the corpus case percentage-points-vs-percent). Percentages commute in\n# algebra but not once every step is rounded to a cent, and a stack with a\n# flat amount does not commute at all (the corpus case\n# successive-percentage-order).\n\ndef round_div(num, den):\n    # num / den (den > 0) to the nearest whole number, halves away from zero\n    num => a\n    if num < 0:\n        0 - num => a\n    a % den => r\n    int((a - r) / den) => q\n    if 2 * r >= den:\n        q + 1 => q\n    if num < 0:\n        return 0 - q\n    return q\n\ndef parse_fixed(t, most):\n    # \"12.34\", \"-5\", \"+0.5\" -> hundredths, or [] (at most `most` digits\n    # before the point and two after)\n    if len(t) == 0:\n        return []\n    1 => sign\n    t => s\n    if s[0] == \"-\" or s[0] == \"+\":\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) > most:\n        return []\n    while len(frac) < 2:\n        frac + \"0\" => frac\n    return [sign * (int(whole) * 100 + int(frac))]\n\ndef hundredths(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 ratio_text(num, den):\n    # num / den hundredths, to two decimals; [text, exact]\n    round_div(num, den) => r\n    return [hundredths(r), r * den == num]\n\ndef parse_step(t):\n    # \"-10%\" or \"+8.25%\" -> [\"%\", basis points]; \"-5.00\" -> [\"flat\", cents]\n    if len(t) > 1 and t[len(t) - 1] == \"%\":\n        parse_fixed(t[0:len(t) - 1], 3) => p\n        if len(p) == 0 or p[0] <= 0 - 10000:\n            return []\n        return [\"%\", p[0]]\n    parse_fixed(t, 7) => a\n    if len(a) == 0:\n        return []\n    return [\"flat\", a[0]]\n\ndef step_text(st):\n    if st[0] == \"%\":\n        \"+\" => sign\n        if st[1] < 0:\n            \"\" => sign\n        return sign + hundredths(st[1]) + \"%\"\n    \"+\" => sign\n    if st[1] < 0:\n        \"\" => sign\n    return sign + hundredths(st[1])\n\ndef apply_rounded(cents, steps):\n    # every step rounded to the cent\n    cents => c\n    for st in steps:\n        if st[0] == \"%\":\n            round_div(c * (10000 + st[1]), 10000) => c\n        else:\n            c + st[1] => c\n    return c\n\ndef apply_exact(cents, steps):\n    # [numerator, denominator] in cents, nothing rounded\n    cents => n\n    1 => d\n    for st in steps:\n        if st[0] == \"%\":\n            n * (10000 + st[1]) => n\n            d * 10000 => d\n        else:\n            n + st[1] * d => n\n    return [n, d]\n\ndef same_fraction(a, b):\n    return a[0] * b[1] == b[0] * a[1]\n\ndef orders(n):\n    # every order of 0..n-1, n from 1 to 4\n    [[]] => out\n    for k in [1:n]:\n        [] => more\n        for o in out:\n            for i in [0:n - 1]:\n                True => free\n                for x in o:\n                    if x == i:\n                        False => free\n                if free:\n                    more + [o + [i]] => more\n        more => out\n    return out\n\ndef ceil_div(a, b):\n    # a / b rounded up, for a >= 0 and b > 0\n    a % b => r\n    int((a - r) / b) => q\n    if r > 0:\n        q + 1 => q\n    return q\n\ndef originals(final, bp):\n    # [lowest, highest]: every whole-cent price from lowest to highest ends\n    # at `final` (at least 0.01) after +bp basis points rounded to the cent;\n    # lowest > highest when no price does. With f = 10000 + bp, a price x\n    # ends at final when final - 1/2 <= x f / 10000 < final + 1/2.\n    10000 + bp => f\n    ceil_div((2 * final - 1) * 5000, f) => lo\n    ceil_div((2 * final + 1) * 5000, f) - 1 => hi\n    return [lo, hi]\n",
      "python": "def round_div(num, den):\n    a = num\n    if num < 0:\n        a = 0 - num\n    r = a % den\n    q = int((a - r) / den)\n    if 2 * r >= den:\n        q = q + 1\n    if num < 0:\n        return 0 - q\n    return q\n\ndef parse_fixed(t, most):\n    if len(t) == 0:\n        return []\n    sign = 1\n    s = t\n    if s[0] == \"-\" or s[0] == \"+\":\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) > most:\n        return []\n    while len(frac) < 2:\n        frac = frac + \"0\"\n    return [sign * (int(whole) * 100 + int(frac))]\n\ndef hundredths(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 ratio_text(num, den):\n    r = round_div(num, den)\n    return [hundredths(r), r * den == num]\n\ndef parse_step(t):\n    if len(t) > 1 and t[len(t) - 1] == \"%\":\n        p = parse_fixed(t[0:len(t) - 1], 3)\n        if len(p) == 0 or p[0] <= 0 - 10000:\n            return []\n        return [\"%\", p[0]]\n    a = parse_fixed(t, 7)\n    if len(a) == 0:\n        return []\n    return [\"flat\", a[0]]\n\ndef step_text(st):\n    if st[0] == \"%\":\n        sign = \"+\"\n        if st[1] < 0:\n            sign = \"\"\n        return sign + hundredths(st[1]) + \"%\"\n    sign = \"+\"\n    if st[1] < 0:\n        sign = \"\"\n    return sign + hundredths(st[1])\n\ndef apply_rounded(cents, steps):\n    c = cents\n    for st in steps:\n        if st[0] == \"%\":\n            c = round_div(c * (10000 + st[1]), 10000)\n        else:\n            c = c + st[1]\n    return c\n\ndef apply_exact(cents, steps):\n    n = cents\n    d = 1\n    for st in steps:\n        if st[0] == \"%\":\n            n = n * (10000 + st[1])\n            d = d * 10000\n        else:\n            n = n + st[1] * d\n    return [n, d]\n\ndef same_fraction(a, b):\n    return a[0] * b[1] == b[0] * a[1]\n\ndef orders(n):\n    out = [[]]\n    for k in range(1, n+1):\n        more = []\n        for o in out:\n            for i in range(0, n):\n                free = True\n                for x in o:\n                    if x == i:\n                        free = False\n                if free:\n                    more = more + [o + [i]]\n        out = more\n    return out\n\ndef ceil_div(a, b):\n    r = a % b\n    q = int((a - r) / b)\n    if r > 0:\n        q = q + 1\n    return q\n\ndef originals(final, bp):\n    f = 10000 + bp\n    lo = ceil_div((2 * final - 1) * 5000, f)\n    hi = ceil_div((2 * final + 1) * 5000, f) - 1\n    return [lo, hi]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n1\nabc\n1\n4\n\n1\n1001\n1\n\n2\n0\n2\n100\n0\n2\n100\n100\n2\n100\n\n3\n12.34\n-10% -20% -25% -5% -1%\n3\n12.34\n-100%\n3\n12.34\n-10% abc\n3\n12.34\n\n3\n\n4\n80\n-5.00\n4\n80\n-120%\n4\n80\n\n4\n\n5\n",
      "screen": "== Percent calculator ==\nPercentage points against percent, a change and its undoing, discounts in every order, and a price worked back - all in whole cents and hundredths of a percent.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 1\nfirst rate in percent> abc\nType a rate from 0 to 1000, at most two decimals.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 1\nfirst rate in percent> 4\nsecond rate in percent> \nCancelled.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 1\nfirst rate in percent> 1001\nType a rate from 0 to 1000, at most two decimals.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 1\nfirst rate in percent> \nCancelled.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 2\nprice> 0\nType a price from 0.01 to 9,999,999.99.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 2\nprice> 100\npercent> 0\nType a percent from 0.01 to 99.99.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 2\nprice> 100\npercent> 100\nType a percent from 0.01 to 99.99.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 2\nprice> 100\npercent> \nCancelled.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 3\nprice> 12.34\n1 to 4 steps, such as -10% -20% -25% or -5.00> -10% -20% -25% -5% -1%\nType 1 to 4 steps: a percent such as -10% or +8.25% (above -100%), or an amount such as -5.00.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 3\nprice> 12.34\n1 to 4 steps, such as -10% -20% -25% or -5.00> -100%\nType 1 to 4 steps: a percent such as -10% or +8.25% (above -100%), or an amount such as -5.00.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 3\nprice> 12.34\n1 to 4 steps, such as -10% -20% -25% or -5.00> -10% abc\nType 1 to 4 steps: a percent such as -10% or +8.25% (above -100%), or an amount such as -5.00.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 3\nprice> 12.34\n1 to 4 steps, such as -10% -20% -25% or -5.00> \nCancelled.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 3\nprice> \nCancelled.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 4\nprice now> 80\nthe change it went through, such as -20% or +8.25%> -5.00\nType a percent above -100%, such as -20% or +8.25%.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 4\nprice now> 80\nthe change it went through, such as -20% or +8.25%> -120%\nType a percent above -100%, such as -20% or +8.25%.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 4\nprice now> 80\nthe change it went through, such as -20% or +8.25%> \nCancelled.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 4\nprice now> \nCancelled.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n4\n6\n1\n6\n4\n1\n0\n5\n2\n100\n20\n2\n12.34\n33.33\n3\n12.34\n-10% -20% -25%\n3\n12.34\n-10% -20% -5.00\n3\n50\n-5.00 -2.50\n4\n80\n-20%\n4\n1.00\n-99%\n4\n1.00\n+200%\n4\n108.25\n+8.25%\n5\n",
      "screen": "== Percent calculator ==\nPercentage points against percent, a change and its undoing, discounts in every order, and a price worked back - all in whole cents and hundredths of a percent.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 1\nfirst rate in percent> 4\nsecond rate in percent> 6\nFrom 4.00% to 6.00%: +2.00 percentage points.\nRelative to 4.00%, that is +50.00%.\nBack to 4.00% is -2.00 points, but about -33.33% relative to 6.00%: points undo themselves, relative changes do not.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 1\nfirst rate in percent> 6\nsecond rate in percent> 4\nFrom 6.00% to 4.00%: -2.00 percentage points.\nRelative to 6.00%, that is about -33.33%.\nBack to 6.00% is +2.00 points, but +50.00% relative to 4.00%: points undo themselves, relative changes do not.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 1\nfirst rate in percent> 0\nsecond rate in percent> 5\nFrom 0.00% to 5.00%: +5.00 percentage points.\nAs a relative change there is none to give: the first rate is zero.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 2\nprice> 100\npercent> 20\n  100.00 up 20.00% is 120.00, then down 20.00% is 96.00.\n  100.00 down 20.00% is 80.00, then up 20.00% is 96.00.\nEither way the two steps multiply the price by (1 + p)(1 - p) = 1 - p squared, losing 4.00% of it before any rounding.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 2\nprice> 12.34\npercent> 33.33\n  12.34 up 33.33% is 16.45, then down 33.33% is 10.97.\n  12.34 down 33.33% is 8.23, then up 33.33% is 10.97.\nEither way the two steps multiply the price by (1 + p)(1 - p) = 1 - p squared, losing about 11.11% of it before any rounding.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 3\nprice> 12.34\n1 to 4 steps, such as -10% -20% -25% or -5.00> -10% -20% -25%\n  order                              rounded each step   exact\n  -10.00% -20.00% -25.00%                     6.67   6.66 (about)\n  -10.00% -25.00% -20.00%                     6.66   6.66 (about)\n  -20.00% -10.00% -25.00%                     6.66   6.66 (about)\n  -20.00% -25.00% -10.00%                     6.66   6.66 (about)\n  -25.00% -10.00% -20.00%                     6.66   6.66 (about)\n  -25.00% -20.00% -10.00%                     6.67   6.66 (about)\n2 different results rounded at each step, 1 exact.\nPercentages alone give the same exact result in any order; rounding at every step is what can tell the orders apart.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 3\nprice> 12.34\n1 to 4 steps, such as -10% -20% -25% or -5.00> -10% -20% -5.00\n  order                              rounded each step   exact\n  -10.00% -20.00% -5.00                       3.89   3.88 (about)\n  -10.00% -5.00 -20.00%                       4.89   4.88 (about)\n  -20.00% -10.00% -5.00                       3.88   3.88 (about)\n  -20.00% -5.00 -10.00%                       4.38   4.38 (about)\n  -5.00 -10.00% -20.00%                       5.29   5.28 (about)\n  -5.00 -20.00% -10.00%                       5.28   5.28 (about)\n6 different results rounded at each step, 4 exact.\nWith a flat amount among percentages the order matters even before rounding: a percentage taken after the flat amount is a percentage of less.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 3\nprice> 50\n1 to 4 steps, such as -10% -20% -25% or -5.00> -5.00 -2.50\n  order                              rounded each step   exact\n  -5.00 -2.50                                42.50   42.50\n  -2.50 -5.00                                42.50   42.50\n1 different result rounded at each step, 1 exact.\nFlat amounts give the same total in any order.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 4\nprice now> 80\nthe change it went through, such as -20% or +8.25%> -20%\nOnly 100.00 comes to 80.00 after -20.00%.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 4\nprice now> 1.00\nthe change it went through, such as -20% or +8.25%> -99%\n100 prices, 99.50 to 100.49, all come to 1.00 after -99.00% rounded to the cent.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 4\nprice now> 1.00\nthe change it went through, such as -20% or +8.25%> +200%\nNo price in whole cents comes to 1.00 after +200.00% rounded to the cent.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 4\nprice now> 108.25\nthe change it went through, such as -20% or +8.25%> +8.25%\nOnly 100.00 comes to 108.25 after +8.25%.\n\n1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "percentage-points-vs-percent",
      "caseId": "242-percentage-points-vs-percent",
      "title": "Points add and reverse; relative changes do not"
    },
    {
      "slug": "successive-percentage-order",
      "caseId": "227-successive-percentage-order",
      "title": "Three tiers of commuting, and only one is safe"
    }
  ],
  "updated": "2026-10-16"
}
