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.
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 calculationspercent.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]