<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P105-percent-calculator/ | updated: 2026-10-16 -->

# P105 Percent calculator

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.

EML-P project `projects/percent-calculator` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/percent-calculator` runs it and `eml project verify projects/percent-calculator` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: percentage-points-vs-percent (https://efficientnewlanguage.org/cases/242-percentage-points-vs-percent/), successive-percentage-order (https://efficientnewlanguage.org/cases/227-successive-percentage-order/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
1
abc
1
4

1
1001
1

2
0
2
100
0
2
100
100
2
100

3
12.34
-10% -20% -25% -5% -1%
3
12.34
-100%
3
12.34
-10% abc
3
12.34

3

4
80
-5.00
4
80
-120%
4
80

4

5
```

Screen:

```text
== Percent calculator ==
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.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 0
Pick a number from 1 to 5.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> x
Pick a number from 1 to 5.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 1
first rate in percent> abc
Type a rate from 0 to 1000, at most two decimals.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 1
first rate in percent> 4
second rate in percent> 
Cancelled.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 1
first rate in percent> 1001
Type a rate from 0 to 1000, at most two decimals.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 1
first rate in percent> 
Cancelled.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 2
price> 0
Type a price from 0.01 to 9,999,999.99.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 2
price> 100
percent> 0
Type a percent from 0.01 to 99.99.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 2
price> 100
percent> 100
Type a percent from 0.01 to 99.99.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 2
price> 100
percent> 
Cancelled.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 3
price> 12.34
1 to 4 steps, such as -10% -20% -25% or -5.00> -10% -20% -25% -5% -1%
Type 1 to 4 steps: a percent such as -10% or +8.25% (above -100%), or an amount such as -5.00.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 3
price> 12.34
1 to 4 steps, such as -10% -20% -25% or -5.00> -100%
Type 1 to 4 steps: a percent such as -10% or +8.25% (above -100%), or an amount such as -5.00.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 3
price> 12.34
1 to 4 steps, such as -10% -20% -25% or -5.00> -10% abc
Type 1 to 4 steps: a percent such as -10% or +8.25% (above -100%), or an amount such as -5.00.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 3
price> 12.34
1 to 4 steps, such as -10% -20% -25% or -5.00> 
Cancelled.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 3
price> 
Cancelled.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 4
price now> 80
the change it went through, such as -20% or +8.25%> -5.00
Type a percent above -100%, such as -20% or +8.25%.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 4
price now> 80
the change it went through, such as -20% or +8.25%> -120%
Type a percent above -100%, such as -20% or +8.25%.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 4
price now> 80
the change it went through, such as -20% or +8.25%> 
Cancelled.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 4
price now> 
Cancelled.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 5
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
4
6
1
6
4
1
0
5
2
100
20
2
12.34
33.33
3
12.34
-10% -20% -25%
3
12.34
-10% -20% -5.00
3
50
-5.00 -2.50
4
80
-20%
4
1.00
-99%
4
1.00
+200%
4
108.25
+8.25%
5
```

Screen:

```text
== Percent calculator ==
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.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 1
first rate in percent> 4
second rate in percent> 6
From 4.00% to 6.00%: +2.00 percentage points.
Relative to 4.00%, that is +50.00%.
Back to 4.00% is -2.00 points, but about -33.33% relative to 6.00%: points undo themselves, relative changes do not.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 1
first rate in percent> 6
second rate in percent> 4
From 6.00% to 4.00%: -2.00 percentage points.
Relative to 6.00%, that is about -33.33%.
Back to 6.00% is +2.00 points, but +50.00% relative to 4.00%: points undo themselves, relative changes do not.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 1
first rate in percent> 0
second rate in percent> 5
From 0.00% to 5.00%: +5.00 percentage points.
As a relative change there is none to give: the first rate is zero.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 2
price> 100
percent> 20
  100.00 up 20.00% is 120.00, then down 20.00% is 96.00.
  100.00 down 20.00% is 80.00, then up 20.00% is 96.00.
Either way the two steps multiply the price by (1 + p)(1 - p) = 1 - p squared, losing 4.00% of it before any rounding.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 2
price> 12.34
percent> 33.33
  12.34 up 33.33% is 16.45, then down 33.33% is 10.97.
  12.34 down 33.33% is 8.23, then up 33.33% is 10.97.
Either way the two steps multiply the price by (1 + p)(1 - p) = 1 - p squared, losing about 11.11% of it before any rounding.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 3
price> 12.34
1 to 4 steps, such as -10% -20% -25% or -5.00> -10% -20% -25%
  order                              rounded each step   exact
  -10.00% -20.00% -25.00%                     6.67   6.66 (about)
  -10.00% -25.00% -20.00%                     6.66   6.66 (about)
  -20.00% -10.00% -25.00%                     6.66   6.66 (about)
  -20.00% -25.00% -10.00%                     6.66   6.66 (about)
  -25.00% -10.00% -20.00%                     6.66   6.66 (about)
  -25.00% -20.00% -10.00%                     6.67   6.66 (about)
2 different results rounded at each step, 1 exact.
Percentages alone give the same exact result in any order; rounding at every step is what can tell the orders apart.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 3
price> 12.34
1 to 4 steps, such as -10% -20% -25% or -5.00> -10% -20% -5.00
  order                              rounded each step   exact
  -10.00% -20.00% -5.00                       3.89   3.88 (about)
  -10.00% -5.00 -20.00%                       4.89   4.88 (about)
  -20.00% -10.00% -5.00                       3.88   3.88 (about)
  -20.00% -5.00 -10.00%                       4.38   4.38 (about)
  -5.00 -10.00% -20.00%                       5.29   5.28 (about)
  -5.00 -20.00% -10.00%                       5.28   5.28 (about)
6 different results rounded at each step, 4 exact.
With a flat amount among percentages the order matters even before rounding: a percentage taken after the flat amount is a percentage of less.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 3
price> 50
1 to 4 steps, such as -10% -20% -25% or -5.00> -5.00 -2.50
  order                              rounded each step   exact
  -5.00 -2.50                                42.50   42.50
  -2.50 -5.00                                42.50   42.50
1 different result rounded at each step, 1 exact.
Flat amounts give the same total in any order.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 4
price now> 80
the change it went through, such as -20% or +8.25%> -20%
Only 100.00 comes to 80.00 after -20.00%.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 4
price now> 1.00
the change it went through, such as -20% or +8.25%> -99%
100 prices, 99.50 to 100.49, all come to 1.00 after -99.00% rounded to the cent.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 4
price now> 1.00
the change it went through, such as -20% or +8.25%> +200%
No price in whole cents comes to 1.00 after +200.00% rounded to the cent.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 4
price now> 108.25
the change it went through, such as -20% or +8.25%> +8.25%
Only 100.00 comes to 108.25 after +8.25%.

1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P105 percent calculator: percentage points against percent, a change up
# and back down, a stack of discounts in every order, and a price worked
# back from what it became.
import percent

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    while len(s) < n:
        s + " " => s
    return s

def lpad(s, n):
    while len(s) < n:
        " " + s => s
    return s

def ask_fixed(prompt, most, low, high, what):
    # [hundredths], [] to cancel, ["!"] after a complaint
    plain(input(prompt + "> ")) => t
    if t == "":
        return []
    percent.parse_fixed(t, most) => v
    if len(v) == 0 or v[0] < low or v[0] > high:
        ("Type " + what + ".") ^0
        return ["!"]
    return v

def rates():
    ask_fixed("first rate in percent", 3, 0, 100000, "a rate from 0 to 1000, at most two decimals") => a
    if len(a) == 0:
        "Cancelled." ^0
        return
    if a[0] == "!":
        return
    ask_fixed("second rate in percent", 3, 0, 100000, "a rate from 0 to 1000, at most two decimals") => b
    if len(b) == 0:
        "Cancelled." ^0
        return
    if b[0] == "!":
        return
    a[0] => x
    b[0] => y
    "+" => sign
    if y < x:
        "" => sign
    ("From " + percent.hundredths(x) + "% to " + percent.hundredths(y) + "%: " + sign + percent.hundredths(y - x) + " percentage points.") ^0
    if x == 0:
        "As a relative change there is none to give: the first rate is zero." ^0
        return
    percent.ratio_text((y - x) * 10000, x) => r
    "about " => how
    if r[1]:
        "" => how
    ("Relative to " + percent.hundredths(x) + "%, that is " + how + sign + r[0] + "%.") ^0
    if y > 0:
        percent.ratio_text((x - y) * 10000, y) => back
        "about " => how2
        if back[1]:
            "" => how2
        "+" => sign2
        if x < y:
            "" => sign2
        ("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

def up_down():
    ask_fixed("price", 7, 1, 999999999, "a price from 0.01 to 9,999,999.99") => p
    if len(p) == 0:
        "Cancelled." ^0
        return
    if p[0] == "!":
        return
    ask_fixed("percent", 3, 1, 9999, "a percent from 0.01 to 99.99") => q
    if len(q) == 0:
        "Cancelled." ^0
        return
    if q[0] == "!":
        return
    p[0] => c
    q[0] => bp
    percent.round_div(c * (10000 + bp), 10000) => up
    percent.round_div(up * (10000 - bp), 10000) => back
    percent.round_div(c * (10000 - bp), 10000) => down
    percent.round_div(down * (10000 + bp), 10000) => again
    percent.hundredths(bp) + "%" => pc
    ("  " + percent.hundredths(c) + " up " + pc + " is " + percent.hundredths(up) + ", then down " + pc + " is " + percent.hundredths(back) + ".") ^0
    ("  " + percent.hundredths(c) + " down " + pc + " is " + percent.hundredths(down) + ", then up " + pc + " is " + percent.hundredths(again) + ".") ^0
    percent.ratio_text(bp * bp, 10000) => sq
    "about " => how
    if sq[1]:
        "" => how
    ("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

def stack():
    ask_fixed("price", 7, 1, 999999999, "a price from 0.01 to 9,999,999.99") => p
    if len(p) == 0:
        "Cancelled." ^0
        return
    if p[0] == "!":
        return
    plain(input("1 to 4 steps, such as -10% -20% -25% or -5.00> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    [] => steps
    "" => w
    True => ok
    for c in t + " ":
        if c == " ":
            if w != "":
                percent.parse_step(w) => st
                if len(st) == 0:
                    False => ok
                else:
                    steps + [st] => steps
            "" => w
        else:
            w + c => w
    if not ok or len(steps) < 1 or len(steps) > 4:
        "Type 1 to 4 steps: a percent such as -10% or +8.25% (above -100%), or an amount such as -5.00." ^0
        return
    p[0] => c
    [] => seen_r
    [] => seen_x
    "  order" => head
    ("  order" + "                              " + "rounded each step   exact") ^0
    for o in percent.orders(len(steps)):
        [] => seq
        "" => label
        for i in o:
            seq + [steps[i]] => seq
            if label != "":
                label + " " => label
            label + percent.step_text(steps[i]) => label
        percent.apply_rounded(c, seq) => r
        percent.apply_exact(c, seq) => x
        True => new_r
        for v in seen_r:
            if v == r:
                False => new_r
        if new_r:
            seen_r + [r] => seen_r
        True => new_x
        for v in seen_x:
            if percent.same_fraction(v, x):
                False => new_x
        if new_x:
            seen_x + [x] => seen_x
        percent.ratio_text(x[0], x[1]) => xt
        "" => mark
        if not xt[1]:
            " (about)" => mark
        ("  " + pad(label, 34) + lpad(percent.hundredths(r), 14) + "   " + xt[0] + mark) ^0
    (str(len(seen_r)) + " different result" + plural(len(seen_r)) + " rounded at each step, " + str(len(seen_x)) + " exact.") ^0
    True => all_pct
    True => all_flat
    for st in steps:
        if st[0] == "%":
            False => all_flat
        else:
            False => all_pct
    if all_flat:
        "Flat amounts give the same total in any order." ^0
    elif all_pct:
        "Percentages alone give the same exact result in any order; rounding at every step is what can tell the orders apart." ^0
    else:
        "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

def plural(n):
    if n == 1:
        return ""
    return "s"

def reverse():
    ask_fixed("price now", 7, 1, 999999999, "a price from 0.01 to 9,999,999.99") => p
    if len(p) == 0:
        "Cancelled." ^0
        return
    if p[0] == "!":
        return
    plain(input("the change it went through, such as -20% or +8.25%> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    percent.parse_step(t) => st
    if len(st) == 0 or st[0] != "%":
        "Type a percent above -100%, such as -20% or +8.25%." ^0
        return
    percent.originals(p[0], st[1]) => r
    percent.step_text(st) => s
    if r[0] > r[1]:
        ("No price in whole cents comes to " + percent.hundredths(p[0]) + " after " + s + " rounded to the cent.") ^0
    elif r[0] == r[1]:
        ("Only " + percent.hundredths(r[0]) + " comes to " + percent.hundredths(p[0]) + " after " + s + ".") ^0
    else:
        (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

"== Percent calculator ==" ^0
"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
True => running
while running:
    "" ^0
    "1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        rates()
    elif choice == "2":
        up_down()
    elif choice == "3":
        stack()
    elif choice == "4":
        reverse()
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import percent

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    while len(s) < n:
        s = s + " "
    return s

def lpad(s, n):
    while len(s) < n:
        s = " " + s
    return s

def ask_fixed(prompt, most, low, high, what):
    t = plain(input(prompt + "> "))
    if t == "":
        return []
    v = percent.parse_fixed(t, most)
    if len(v) == 0 or v[0] < low or v[0] > high:
        print("Type " + what + ".")
        return ["!"]
    return v

def rates():
    a = ask_fixed("first rate in percent", 3, 0, 100000, "a rate from 0 to 1000, at most two decimals")
    if len(a) == 0:
        print("Cancelled.")
        return
    if a[0] == "!":
        return
    b = ask_fixed("second rate in percent", 3, 0, 100000, "a rate from 0 to 1000, at most two decimals")
    if len(b) == 0:
        print("Cancelled.")
        return
    if b[0] == "!":
        return
    x = a[0]
    y = b[0]
    sign = "+"
    if y < x:
        sign = ""
    print("From " + percent.hundredths(x) + "% to " + percent.hundredths(y) + "%: " + sign + percent.hundredths(y - x) + " percentage points.")
    if x == 0:
        print("As a relative change there is none to give: the first rate is zero.")
        return
    r = percent.ratio_text((y - x) * 10000, x)
    how = "about "
    if r[1]:
        how = ""
    print("Relative to " + percent.hundredths(x) + "%, that is " + how + sign + r[0] + "%.")
    if y > 0:
        back = percent.ratio_text((x - y) * 10000, y)
        how2 = "about "
        if back[1]:
            how2 = ""
        sign2 = "+"
        if x < y:
            sign2 = ""
        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.")

def up_down():
    p = ask_fixed("price", 7, 1, 999999999, "a price from 0.01 to 9,999,999.99")
    if len(p) == 0:
        print("Cancelled.")
        return
    if p[0] == "!":
        return
    q = ask_fixed("percent", 3, 1, 9999, "a percent from 0.01 to 99.99")
    if len(q) == 0:
        print("Cancelled.")
        return
    if q[0] == "!":
        return
    c = p[0]
    bp = q[0]
    up = percent.round_div(c * (10000 + bp), 10000)
    back = percent.round_div(up * (10000 - bp), 10000)
    down = percent.round_div(c * (10000 - bp), 10000)
    again = percent.round_div(down * (10000 + bp), 10000)
    pc = percent.hundredths(bp) + "%"
    print("  " + percent.hundredths(c) + " up " + pc + " is " + percent.hundredths(up) + ", then down " + pc + " is " + percent.hundredths(back) + ".")
    print("  " + percent.hundredths(c) + " down " + pc + " is " + percent.hundredths(down) + ", then up " + pc + " is " + percent.hundredths(again) + ".")
    sq = percent.ratio_text(bp * bp, 10000)
    how = "about "
    if sq[1]:
        how = ""
    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.")

def stack():
    p = ask_fixed("price", 7, 1, 999999999, "a price from 0.01 to 9,999,999.99")
    if len(p) == 0:
        print("Cancelled.")
        return
    if p[0] == "!":
        return
    t = plain(input("1 to 4 steps, such as -10% -20% -25% or -5.00> "))
    if t == "":
        print("Cancelled.")
        return
    steps = []
    w = ""
    ok = True
    for c in t + " ":
        if c == " ":
            if w != "":
                st = percent.parse_step(w)
                if len(st) == 0:
                    ok = False
                else:
                    steps = steps + [st]
            w = ""
        else:
            w = w + c
    if not ok or len(steps) < 1 or len(steps) > 4:
        print("Type 1 to 4 steps: a percent such as -10% or +8.25% (above -100%), or an amount such as -5.00.")
        return
    c = p[0]
    seen_r = []
    seen_x = []
    head = "  order"
    print("  order" + "                              " + "rounded each step   exact")
    for o in percent.orders(len(steps)):
        seq = []
        label = ""
        for i in o:
            seq = seq + [steps[i]]
            if label != "":
                label = label + " "
            label = label + percent.step_text(steps[i])
        r = percent.apply_rounded(c, seq)
        x = percent.apply_exact(c, seq)
        new_r = True
        for v in seen_r:
            if v == r:
                new_r = False
        if new_r:
            seen_r = seen_r + [r]
        new_x = True
        for v in seen_x:
            if percent.same_fraction(v, x):
                new_x = False
        if new_x:
            seen_x = seen_x + [x]
        xt = percent.ratio_text(x[0], x[1])
        mark = ""
        if not xt[1]:
            mark = " (about)"
        print("  " + pad(label, 34) + lpad(percent.hundredths(r), 14) + "   " + xt[0] + mark)
    print(str(len(seen_r)) + " different result" + plural(len(seen_r)) + " rounded at each step, " + str(len(seen_x)) + " exact.")
    all_pct = True
    all_flat = True
    for st in steps:
        if st[0] == "%":
            all_flat = False
        else:
            all_pct = False
    if all_flat:
        print("Flat amounts give the same total in any order.")
    elif all_pct:
        print("Percentages alone give the same exact result in any order; rounding at every step is what can tell the orders apart.")
    else:
        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.")

def plural(n):
    if n == 1:
        return ""
    return "s"

def reverse():
    p = ask_fixed("price now", 7, 1, 999999999, "a price from 0.01 to 9,999,999.99")
    if len(p) == 0:
        print("Cancelled.")
        return
    if p[0] == "!":
        return
    t = plain(input("the change it went through, such as -20% or +8.25%> "))
    if t == "":
        print("Cancelled.")
        return
    st = percent.parse_step(t)
    if len(st) == 0 or st[0] != "%":
        print("Type a percent above -100%, such as -20% or +8.25%.")
        return
    r = percent.originals(p[0], st[1])
    s = percent.step_text(st)
    if r[0] > r[1]:
        print("No price in whole cents comes to " + percent.hundredths(p[0]) + " after " + s + " rounded to the cent.")
    elif r[0] == r[1]:
        print("Only " + percent.hundredths(r[0]) + " comes to " + percent.hundredths(p[0]) + " after " + s + ".")
    else:
        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.")

print("== Percent calculator ==")
print("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.")
running = True
while running:
    print("")
    print("1) two rates  2) up and down  3) a stack of discounts  4) work a price back  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        rates()
    elif choice == "2":
        up_down()
    elif choice == "3":
        stack()
    elif choice == "4":
        reverse()
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### percent.eml

```eml
# P105 percent calculator - money in whole cents, percentages in basis
# points (hundredths of a percent), so nothing passes through a float.
# Percentage points add and undo; relative changes multiply and do not
# (the corpus case percentage-points-vs-percent). Percentages commute in
# algebra but not once every step is rounded to a cent, and a stack with a
# flat amount does not commute at all (the corpus case
# successive-percentage-order).

def round_div(num, den):
    # num / den (den > 0) to the nearest whole number, halves away from zero
    num => a
    if num < 0:
        0 - num => a
    a % den => r
    int((a - r) / den) => q
    if 2 * r >= den:
        q + 1 => q
    if num < 0:
        return 0 - q
    return q

def parse_fixed(t, most):
    # "12.34", "-5", "+0.5" -> hundredths, or [] (at most `most` digits
    # before the point and two after)
    if len(t) == 0:
        return []
    1 => sign
    t => s
    if s[0] == "-" or s[0] == "+":
        if s[0] == "-":
            0 - 1 => sign
        s[1:len(s)] => s
    "" => whole
    "" => frac
    False => dot
    for c in s:
        if c == ".":
            if dot:
                return []
            True => dot
        elif c >= "0" and c <= "9":
            if dot:
                frac + c => frac
            else:
                whole + c => whole
        else:
            return []
    if whole == "" or len(frac) > 2 or (dot and frac == "") or len(whole) > most:
        return []
    while len(frac) < 2:
        frac + "0" => frac
    return [sign * (int(whole) * 100 + int(frac))]

def hundredths(c):
    # 123456 -> "1,234.56", -5 -> "-0.05"
    "" => sign
    c => a
    if c < 0:
        "-" => sign
        0 - c => a
    a % 100 => small
    int((a - small) / 100) => big
    str(big) => s
    "" => out
    0 => k
    for i in [0:len(s) - 1]:
        s[len(s) - 1 - i] + out => out
        k + 1 => k
        if k % 3 == 0 and i < len(s) - 1:
            "," + out => out
    if small < 10:
        return sign + out + ".0" + str(small)
    return sign + out + "." + str(small)

def ratio_text(num, den):
    # num / den hundredths, to two decimals; [text, exact]
    round_div(num, den) => r
    return [hundredths(r), r * den == num]

def parse_step(t):
    # "-10%" or "+8.25%" -> ["%", basis points]; "-5.00" -> ["flat", cents]
    if len(t) > 1 and t[len(t) - 1] == "%":
        parse_fixed(t[0:len(t) - 1], 3) => p
        if len(p) == 0 or p[0] <= 0 - 10000:
            return []
        return ["%", p[0]]
    parse_fixed(t, 7) => a
    if len(a) == 0:
        return []
    return ["flat", a[0]]

def step_text(st):
    if st[0] == "%":
        "+" => sign
        if st[1] < 0:
            "" => sign
        return sign + hundredths(st[1]) + "%"
    "+" => sign
    if st[1] < 0:
        "" => sign
    return sign + hundredths(st[1])

def apply_rounded(cents, steps):
    # every step rounded to the cent
    cents => c
    for st in steps:
        if st[0] == "%":
            round_div(c * (10000 + st[1]), 10000) => c
        else:
            c + st[1] => c
    return c

def apply_exact(cents, steps):
    # [numerator, denominator] in cents, nothing rounded
    cents => n
    1 => d
    for st in steps:
        if st[0] == "%":
            n * (10000 + st[1]) => n
            d * 10000 => d
        else:
            n + st[1] * d => n
    return [n, d]

def same_fraction(a, b):
    return a[0] * b[1] == b[0] * a[1]

def orders(n):
    # every order of 0..n-1, n from 1 to 4
    [[]] => out
    for k in [1:n]:
        [] => more
        for o in out:
            for i in [0:n - 1]:
                True => free
                for x in o:
                    if x == i:
                        False => free
                if free:
                    more + [o + [i]] => more
        more => out
    return out

def ceil_div(a, b):
    # a / b rounded up, for a >= 0 and b > 0
    a % b => r
    int((a - r) / b) => q
    if r > 0:
        q + 1 => q
    return q

def originals(final, bp):
    # [lowest, highest]: every whole-cent price from lowest to highest ends
    # at `final` (at least 0.01) after +bp basis points rounded to the cent;
    # lowest > highest when no price does. With f = 10000 + bp, a price x
    # ends at final when final - 1/2 <= x f / 10000 < final + 1/2.
    10000 + bp => f
    ceil_div((2 * final - 1) * 5000, f) => lo
    ceil_div((2 * final + 1) * 5000, f) - 1 => hi
    return [lo, hi]
```

Python projection of percent.eml:

```python
def round_div(num, den):
    a = num
    if num < 0:
        a = 0 - num
    r = a % den
    q = int((a - r) / den)
    if 2 * r >= den:
        q = q + 1
    if num < 0:
        return 0 - q
    return q

def parse_fixed(t, most):
    if len(t) == 0:
        return []
    sign = 1
    s = t
    if s[0] == "-" or s[0] == "+":
        if s[0] == "-":
            sign = 0 - 1
        s = s[1:len(s)]
    whole = ""
    frac = ""
    dot = False
    for c in s:
        if c == ".":
            if dot:
                return []
            dot = True
        elif c >= "0" and c <= "9":
            if dot:
                frac = frac + c
            else:
                whole = whole + c
        else:
            return []
    if whole == "" or len(frac) > 2 or dot and frac == "" or len(whole) > most:
        return []
    while len(frac) < 2:
        frac = frac + "0"
    return [sign * (int(whole) * 100 + int(frac))]

def hundredths(c):
    sign = ""
    a = c
    if c < 0:
        sign = "-"
        a = 0 - c
    small = a % 100
    big = int((a - small) / 100)
    s = str(big)
    out = ""
    k = 0
    for i in range(0, len(s)):
        out = s[len(s) - 1 - i] + out
        k = k + 1
        if k % 3 == 0 and i < len(s) - 1:
            out = "," + out
    if small < 10:
        return sign + out + ".0" + str(small)
    return sign + out + "." + str(small)

def ratio_text(num, den):
    r = round_div(num, den)
    return [hundredths(r), r * den == num]

def parse_step(t):
    if len(t) > 1 and t[len(t) - 1] == "%":
        p = parse_fixed(t[0:len(t) - 1], 3)
        if len(p) == 0 or p[0] <= 0 - 10000:
            return []
        return ["%", p[0]]
    a = parse_fixed(t, 7)
    if len(a) == 0:
        return []
    return ["flat", a[0]]

def step_text(st):
    if st[0] == "%":
        sign = "+"
        if st[1] < 0:
            sign = ""
        return sign + hundredths(st[1]) + "%"
    sign = "+"
    if st[1] < 0:
        sign = ""
    return sign + hundredths(st[1])

def apply_rounded(cents, steps):
    c = cents
    for st in steps:
        if st[0] == "%":
            c = round_div(c * (10000 + st[1]), 10000)
        else:
            c = c + st[1]
    return c

def apply_exact(cents, steps):
    n = cents
    d = 1
    for st in steps:
        if st[0] == "%":
            n = n * (10000 + st[1])
            d = d * 10000
        else:
            n = n + st[1] * d
    return [n, d]

def same_fraction(a, b):
    return a[0] * b[1] == b[0] * a[1]

def orders(n):
    out = [[]]
    for k in range(1, n+1):
        more = []
        for o in out:
            for i in range(0, n):
                free = True
                for x in o:
                    if x == i:
                        free = False
                if free:
                    more = more + [o + [i]]
        out = more
    return out

def ceil_div(a, b):
    r = a % b
    q = int((a - r) / b)
    if r > 0:
        q = q + 1
    return q

def originals(final, bp):
    f = 10000 + bp
    lo = ceil_div((2 * final - 1) * 5000, f)
    hi = ceil_div((2 * final + 1) * 5000, f) - 1
    return [lo, hi]
```

## README

# P105 - Percent calculator

Percentage points against percent, a change and its undoing, a stack of
discounts in every order, and a price worked back from what it became.
Everything is in whole cents and hundredths of a percent, so no float is
involved.

- `main.eml` - the menu and the four calculations
- `percent.eml` - rounding to a whole number, reading amounts and steps,
  the steps applied with and without rounding, every order of the steps,
  and the range of prices that end at a given one

How each part works:

- Two rates, as in the corpus case `percentage-points-vs-percent`: 4% to
  6% is +2 percentage points and +50%. Going back, it is -2 points, but
  about -33.33%. Points undo themselves; relative changes do not.
- Up and down: a price up p% and then down p%, or down and then up, is
  multiplied by (1 + p)(1 - p) = 1 - p², so either way it loses p² before
  any rounding.
- A stack of steps, each a percent or a flat amount, applied in every
  order: each step is rounded to the cent (half away from zero), and
  beside it is the exact result. As in the corpus case
  `successive-percentage-order`:
  - flat amounts give the same total in any order;
  - percentages alone give the same exact result in any order, but
    rounding at every step can tell orders apart (6.67 against 6.66 on
    12.34 with -10%, -20% and -25%);
  - with a flat amount among percentages, the order matters even before
    rounding.
- Working a price back: every whole-cent price that comes to the given one
  after a percent change, rounded to the cent. It can be one price, a
  range (100 prices end at 1.00 after -99%), or none (no price ends at 1.00
  after +200%, since three times a whole number of cents jumps over it).

What is checked: menu choices 1 to 5; rates from 0% to 1000%; prices
from 0.01 to 9,999,999.99; a percent from 0.01% to 99.99% for up and
down; 1 to 4 steps, each a percent above -100% or an amount, at most two
decimals. An empty answer cancels.

Sessions: `sessions/basic.in` gives:
- 4% to 6% and 6% to 4%, and a first rate of 0;
- 100.00 and 20%, then 12.34 and 33.33%, up and down;
- 12.34 with -10%, -20% and -25%; with -10%, -20% and -5.00; and 50.00
  with -5.00 and -2.50;
- prices worked back from 80.00 after -20%, 1.00 after -99%, 1.00 after
  +200%, and 108.25 after +8.25%.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- rates abc, a second-rate cancel, 1001, and a cancel;
- up and down with a price of 0, percents of 0 and 100, and a cancel;
- stacks of five steps, of -100%, and with a word, then two cancels;
- working back with a flat amount and with -120%, then two cancels.

Built on the verified corpus cases `percentage-points-vs-percent`
(points add and undo; relative changes multiply and do not) and
`successive-percentage-order` (flats commute, percentages only until
rounding, a mixture not at all).
