Project 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.

2 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-16

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

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).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== 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.
What was typed (48 lines)
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

basic

interpreter: byte-equal
== 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.
What was typed (37 lines)
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

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

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 (main.py)
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 (percent.py)
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]

Built on these corpus cases

ΣEML 2026

Efficient New Language — a high-density semantic overlay for humans and AI agents.

This site’s demos run on the real EML toolchain, in your browser.

© 2026 EveMissLab (一言諾科技有限公司) / Neo.K. All rights reserved.