Invoice rounding
A supplier's invoice with tax on every line, each line rounded half-up to the cent: rounded lines add up to more than the invoice is worth, and rounded shares of it to 101%.
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
A supplier's invoice with 8.25% tax on every line, each line rounded half-up to the cent, and what the total is a sum of.
main.eml- the menu, the invoice, the month's invoices, the shares by category, and a line of your owninvoice.eml- the tax, the lines and their categories, exact amounts in ten-thousandths of a cent, rounding to the cent, the month's run and whole-percent shares
How each part works:
- Amounts are kept exactly, as whole ten-thousandths of a cent, so no float is involved; a line is rounded half-up to the cent, as an invoice must print it.
- As in the corpus case
the-rounding-was-applied-at-every-step, a total of rounded lines carries one rounding per line. Invoice 1001 has six lines, all six rounded up by 0.43 to 0.49 of a cent: rounded line by line it is 135.08, added exactly and rounded once 135.05. - As in
the-total-was-the-sum-of-the-rounded-rows, the month's run of 300 invoices, 1,800 lines, comes to 23 cents more line by line than added exactly: 878 lines rounded up and 922 down, and the errors do not cancel out. No line is off by more than half a cent; 1,800 lines could drift as far as 9.00. - As in
the-shares-were-rounded-and-summed-to-101, the invoice's three categories are 44.70%, 24.59% and 30.71% of it; each rounded to a whole percent, 45%, 25% and 31%, they add up to 101%.
What is checked: menu choices 1 to 5; a quantity from 1 to 999; a unit price written as 2, 2.4 or 2.49, from 0.01 to 999.99. An empty answer cancels.
Sessions: sessions/basic.in gives: - the invoice, the month's invoices and the shares; - lines of your own: 3 at 18.89 (rounded up), 1 at 1.00 (rounded down) and 4 at 1.00 (a whole number of cents).
sessions/bad-input.in gives: - menu choices 0 and x; - quantities: a cancel, 0, 1000 and x; - then prices with a cancel, 0, 1000, .5, 2.499 and x; - 2 at 2.5, rounded down by a quarter of a cent.
Built on the verified corpus cases the-rounding-was-applied-at-every-step (twelve amounts rounded one by one against rounded once), the-total-was-the-sum-of-the-rounded-rows (90,000 rows correct to the cent, and a total 25 cents off) and the-shares-were-rounded-and-summed-to-101 (three shares, each rounded correctly, adding up to 101).
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== Invoice rounding ==
Every line rounded to the cent, and what the total is a sum of.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 0
Pick a number from 1 to 5.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> x
Pick a number from 1 to 5.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)>
Cancelled.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 0
Type a whole number from 1 to 999.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 1000
Type a whole number from 1 to 999.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> x
Type a whole number from 1 to 999.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 2
unit price (0.01 to 999.99)>
Cancelled.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 2
unit price (0.01 to 999.99)> 0
Type a price such as 2.49, from 0.01 to 999.99.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 2
unit price (0.01 to 999.99)> 1000
Type a price such as 2.49, from 0.01 to 999.99.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 2
unit price (0.01 to 999.99)> .5
Type a price such as 2.49, from 0.01 to 999.99.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 2
unit price (0.01 to 999.99)> 2.499
Type a price such as 2.49, from 0.01 to 999.99.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 2
unit price (0.01 to 999.99)> x
Type a price such as 2.49, from 0.01 to 999.99.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 2
unit price (0.01 to 999.99)> 2.5
With tax: 5.412500 exactly, 5.41 on the invoice.
Rounded down by 0.2500 of a cent.
Ten lines like it: 54.10 rounded line by line, 54.13 rounded once.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 5
Bye.
What was typed (32 lines)
0
x
4
4
0
4
1000
4
x
4
2
4
2
0
4
2
1000
4
2
.5
4
2
2.499
4
2
x
4
2
2.5
5
basic
interpreter: byte-equal== Invoice rounding ==
Every line rounded to the cent, and what the total is a sum of.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 1
Invoice 1001, with 8.25% tax on every line, in dollars:
item qty price with tax, exact on the invoice
espresso beans 1 kg 2 17.79 38.515350 38.52
decaf beans 1 kg 1 20.19 21.855675 21.86
whole milk 2 l 8 2.69 23.295400 23.30
oat milk 1 l 4 2.29 9.915700 9.92
croissants 14 1.39 21.065450 21.07
muffins 13 1.45 20.405125 20.41
Rounding each line to the cent, then adding: 135.08.
Adding the exact lines, then rounding once: 135.05.
Every line is right to the cent, and 6 of the 6 were rounded up: the lines add up to 3 cents more than the invoice is worth.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 2
The month's run: 300 invoices of the same six items in varying quantities, 1,800 lines.
Rounding each line, then adding: 45,266.61.
Adding exactly, then rounding once: 45,266.38.
878 lines were rounded up and 922 down.
The lines come to 23 cents more than the run is worth. No line is off by more than half a cent, and the errors do not cancel out: 1,800 lines could drift as far as 9.00 either way.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 3
Invoice 1001 by category, of 135.08, each share rounded half-up to a whole percent:
coffee 60.38 44.70% 45%
dairy 33.22 24.59% 25%
bakery 41.48 30.71% 31%
The rounded shares add up to 101%, and the three categories are the whole invoice: each share is right to the percent, and their sum is not.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 3
unit price (0.01 to 999.99)> 18.89
With tax: 61.345275 exactly, 61.35 on the invoice.
Rounded up by 0.4725 of a cent.
Ten lines like it: 613.50 rounded line by line, 613.45 rounded once.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 1
unit price (0.01 to 999.99)> 1
With tax: 1.082500 exactly, 1.08 on the invoice.
Rounded down by 0.2500 of a cent.
Ten lines like it: 10.80 rounded line by line, 10.83 rounded once.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 4
quantity (1 to 999)> 4
unit price (0.01 to 999.99)> 1
With tax: 4.330000 exactly, 4.33 on the invoice.
A whole number of cents: nothing to round.
Ten lines like it: 43.30 rounded line by line, 43.30 rounded once.
1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit
choice> 5
Bye.
What was typed (13 lines)
1
2
3
4
3
18.89
4
1
1
4
4
1
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# P163 invoice rounding: one invoice rounded line by line and rounded once,
# the month's run of invoices, the shares by category, and a line of your
# own.
import invoice
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):
s => out
while len(out) < n:
out + " " => out
return out
def lpad(s, n):
s => out
while len(out) < n:
" " + out => out
return out
def digits(t):
for c in t:
if c < "0" or c > "9":
return False
return len(t) > 0
def commas(n):
str(n) => s
"" => out
0 => k
len(s) => i
while i > 0:
if k == 3:
"," + out => out
0 => k
s[i - 1] + out => out
k + 1 => k
i - 1 => i
return out
def fraction(n, places):
# n as exactly that many decimal places, n >= 0
str(n) => s
while len(s) < places:
"0" + s => s
return s
def money(c):
# c cents, c >= 0
return commas(int((c - c % 100) / 100)) + "." + fraction(c % 100, 2)
def exact_money(x):
# x ten-thousandths of a cent, as dollars with six places
return commas(int((x - x % 1000000) / 1000000)) + "." + fraction(x % 1000000, 6)
def part_of_cent(e):
# e ten-thousandths of a cent, e >= 0, as a part of a cent
return "0." + fraction(e, 4)
def show_invoice():
"Invoice 1001, with 8.25% tax on every line, in dollars:" ^0
(" " + pad("item", 21) + lpad("qty", 4) + lpad("price", 9) + lpad("with tax, exact", 18) + lpad("on the invoice", 16)) ^0
0 => each
0 => whole
0 => up
for ln in invoice.lines():
invoice.exact(ln[2], ln[3]) => x
invoice.cents(x) => c
each + c => each
whole + x => whole
if c * 10000 > x:
up + 1 => up
(" " + pad(ln[0], 21) + lpad(str(ln[2]), 4) + lpad(money(ln[3]), 9) + lpad(exact_money(x), 18) + lpad(money(c), 16)) ^0
("Rounding each line to the cent, then adding: " + money(each) + ".") ^0
("Adding the exact lines, then rounding once: " + money(invoice.cents(whole)) + ".") ^0
("Every line is right to the cent, and " + str(up) + " of the " + str(len(invoice.lines())) + " were rounded up: the lines add up to " + str(each - invoice.cents(whole)) + " cents more than the invoice is worth.") ^0
def show_run():
invoice.run() => t
invoice.run_size() * len(invoice.lines()) => n
("The month's run: " + str(invoice.run_size()) + " invoices of the same six items in varying quantities, " + commas(n) + " lines.") ^0
("Rounding each line, then adding: " + money(t[0]) + ".") ^0
("Adding exactly, then rounding once: " + money(invoice.cents(t[1])) + ".") ^0
(str(t[2]) + " lines were rounded up and " + str(t[3]) + " down.") ^0
("The lines come to " + str(t[0] - invoice.cents(t[1])) + " cents more than the run is worth. No line is off by more than half a cent, and the errors do not cancel out: " + commas(n) + " lines could drift as far as " + money(int(n / 2)) + " either way.") ^0
def show_shares():
[] => sums
0 => total
for cat in invoice.categories():
0 => v
for ln in invoice.lines():
if ln[1] == cat:
v + invoice.cents(invoice.exact(ln[2], ln[3])) => v
sums + [v] => sums
total + v => total
("Invoice 1001 by category, of " + money(total) + ", each share rounded half-up to a whole percent:") ^0
0 => s
0 => i
for cat in invoice.categories():
invoice.share_hundredths(sums[i], total) => h
invoice.share(sums[i], total) => p
s + p => s
(" " + pad(cat, 8) + lpad(money(sums[i]), 8) + lpad(str(int((h - h % 100) / 100)) + "." + fraction(h % 100, 2) + "%", 10) + lpad(str(p) + "%", 6)) ^0
i + 1 => i
("The rounded shares add up to " + str(s) + "%, and the three categories are the whole invoice: each share is right to the percent, and their sum is not.") ^0
def price(t):
# cents for a price typed as 2, 2.4 or 2.49; -1 if it is not one
0 => dot
0 => i
0 => at
for c in t:
if c == ".":
dot + 1 => dot
i => at
elif c < "0" or c > "9":
return 0 - 1
i + 1 => i
if dot == 0:
if len(t) < 1 or len(t) > 3:
return 0 - 1
return int(t) * 100
if dot > 1 or at < 1 or at > 3 or len(t) - at - 1 < 1 or len(t) - at - 1 > 2:
return 0 - 1
t[at + 1:] => f
if len(f) == 1:
f + "0" => f
return int(t[0:at]) * 100 + int(f)
def own():
plain(input("quantity (1 to 999)> ")) => q
if q == "":
"Cancelled." ^0
return
if len(q) > 3 or not digits(q) or int(q) < 1:
"Type a whole number from 1 to 999." ^0
return
plain(input("unit price (0.01 to 999.99)> ")) => t
if t == "":
"Cancelled." ^0
return
price(t) => p
if p < 1:
"Type a price such as 2.49, from 0.01 to 999.99." ^0
return
invoice.exact(int(q), p) => x
invoice.cents(x) => c
c * 10000 - x => e
("With tax: " + exact_money(x) + " exactly, " + money(c) + " on the invoice.") ^0
if e > 0:
("Rounded up by " + part_of_cent(e) + " of a cent.") ^0
elif e < 0:
("Rounded down by " + part_of_cent(0 - e) + " of a cent.") ^0
else:
"A whole number of cents: nothing to round." ^0
("Ten lines like it: " + money(10 * c) + " rounded line by line, " + money(invoice.cents(10 * x)) + " rounded once.") ^0
"== Invoice rounding ==" ^0
"Every line rounded to the cent, and what the total is a sum of." ^0
True => running
while running:
"" ^0
"1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit" ^0
plain(input("choice> ")) => choice
if choice == "1":
show_invoice()
elif choice == "2":
show_run()
elif choice == "3":
show_shares()
elif choice == "4":
own()
elif choice == "5":
False => running
else:
"Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import invoice
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):
out = s
while len(out) < n:
out = out + " "
return out
def lpad(s, n):
out = s
while len(out) < n:
out = " " + out
return out
def digits(t):
for c in t:
if c < "0" or c > "9":
return False
return len(t) > 0
def commas(n):
s = str(n)
out = ""
k = 0
i = len(s)
while i > 0:
if k == 3:
out = "," + out
k = 0
out = s[i - 1] + out
k = k + 1
i = i - 1
return out
def fraction(n, places):
s = str(n)
while len(s) < places:
s = "0" + s
return s
def money(c):
return commas(int((c - c % 100) / 100)) + "." + fraction(c % 100, 2)
def exact_money(x):
return commas(int((x - x % 1000000) / 1000000)) + "." + fraction(x % 1000000, 6)
def part_of_cent(e):
return "0." + fraction(e, 4)
def show_invoice():
print("Invoice 1001, with 8.25% tax on every line, in dollars:")
print(" " + pad("item", 21) + lpad("qty", 4) + lpad("price", 9) + lpad("with tax, exact", 18) + lpad("on the invoice", 16))
each = 0
whole = 0
up = 0
for ln in invoice.lines():
x = invoice.exact(ln[2], ln[3])
c = invoice.cents(x)
each = each + c
whole = whole + x
if c * 10000 > x:
up = up + 1
print(" " + pad(ln[0], 21) + lpad(str(ln[2]), 4) + lpad(money(ln[3]), 9) + lpad(exact_money(x), 18) + lpad(money(c), 16))
print("Rounding each line to the cent, then adding: " + money(each) + ".")
print("Adding the exact lines, then rounding once: " + money(invoice.cents(whole)) + ".")
print("Every line is right to the cent, and " + str(up) + " of the " + str(len(invoice.lines())) + " were rounded up: the lines add up to " + str(each - invoice.cents(whole)) + " cents more than the invoice is worth.")
def show_run():
t = invoice.run()
n = invoice.run_size() * len(invoice.lines())
print("The month's run: " + str(invoice.run_size()) + " invoices of the same six items in varying quantities, " + commas(n) + " lines.")
print("Rounding each line, then adding: " + money(t[0]) + ".")
print("Adding exactly, then rounding once: " + money(invoice.cents(t[1])) + ".")
print(str(t[2]) + " lines were rounded up and " + str(t[3]) + " down.")
print("The lines come to " + str(t[0] - invoice.cents(t[1])) + " cents more than the run is worth. No line is off by more than half a cent, and the errors do not cancel out: " + commas(n) + " lines could drift as far as " + money(int(n / 2)) + " either way.")
def show_shares():
sums = []
total = 0
for cat in invoice.categories():
v = 0
for ln in invoice.lines():
if ln[1] == cat:
v = v + invoice.cents(invoice.exact(ln[2], ln[3]))
sums = sums + [v]
total = total + v
print("Invoice 1001 by category, of " + money(total) + ", each share rounded half-up to a whole percent:")
s = 0
i = 0
for cat in invoice.categories():
h = invoice.share_hundredths(sums[i], total)
p = invoice.share(sums[i], total)
s = s + p
print(" " + pad(cat, 8) + lpad(money(sums[i]), 8) + lpad(str(int((h - h % 100) / 100)) + "." + fraction(h % 100, 2) + "%", 10) + lpad(str(p) + "%", 6))
i = i + 1
print("The rounded shares add up to " + str(s) + "%, and the three categories are the whole invoice: each share is right to the percent, and their sum is not.")
def price(t):
dot = 0
i = 0
at = 0
for c in t:
if c == ".":
dot = dot + 1
at = i
elif c < "0" or c > "9":
return 0 - 1
i = i + 1
if dot == 0:
if len(t) < 1 or len(t) > 3:
return 0 - 1
return int(t) * 100
if dot > 1 or at < 1 or at > 3 or len(t) - at - 1 < 1 or len(t) - at - 1 > 2:
return 0 - 1
f = t[at + 1:]
if len(f) == 1:
f = f + "0"
return int(t[0:at]) * 100 + int(f)
def own():
q = plain(input("quantity (1 to 999)> "))
if q == "":
print("Cancelled.")
return
if len(q) > 3 or not digits(q) or int(q) < 1:
print("Type a whole number from 1 to 999.")
return
t = plain(input("unit price (0.01 to 999.99)> "))
if t == "":
print("Cancelled.")
return
p = price(t)
if p < 1:
print("Type a price such as 2.49, from 0.01 to 999.99.")
return
x = invoice.exact(int(q), p)
c = invoice.cents(x)
e = c * 10000 - x
print("With tax: " + exact_money(x) + " exactly, " + money(c) + " on the invoice.")
if e > 0:
print("Rounded up by " + part_of_cent(e) + " of a cent.")
elif e < 0:
print("Rounded down by " + part_of_cent(0 - e) + " of a cent.")
else:
print("A whole number of cents: nothing to round.")
print("Ten lines like it: " + money(10 * c) + " rounded line by line, " + money(invoice.cents(10 * x)) + " rounded once.")
print("== Invoice rounding ==")
print("Every line rounded to the cent, and what the total is a sum of.")
running = True
while running:
print("")
print("1) the invoice 2) the month's invoices 3) shares by category 4) a line of your own 5) quit")
choice = plain(input("choice> "))
if choice == "1":
show_invoice()
elif choice == "2":
show_run()
elif choice == "3":
show_shares()
elif choice == "4":
own()
elif choice == "5":
running = False
else:
print("Pick a number from 1 to 5.")
print("Bye.")
invoice.eml
eml# P163 invoice rounding - a supplier's invoice with 8.25% tax on every line,
# kept exactly in ten-thousandths of a cent. As in the corpus cases
# the-rounding-was-applied-at-every-step and
# the-total-was-the-sum-of-the-rounded-rows, every line is rounded half-up to
# the cent, which is right, and a total made of rounded lines carries one
# rounding per line where the exact total rounded once carries one. And as in
# the-shares-were-rounded-and-summed-to-101, shares rounded one by one need
# not add up to the whole.
def tax():
# 8.25%, as 10825 per 10000 of the price with tax
return 10825
def lines():
# [item, category, quantity, unit price in cents]
return [["espresso beans 1 kg", "coffee", 2, 1779], ["decaf beans 1 kg", "coffee", 1, 2019],
["whole milk 2 l", "dairy", 8, 269], ["oat milk 1 l", "dairy", 4, 229],
["croissants", "bakery", 14, 139], ["muffins", "bakery", 13, 145]]
def categories():
return ["coffee", "dairy", "bakery"]
def exact(q, p):
# a line with tax, in ten-thousandths of a cent
return q * p * tax()
def cents(x):
# x ten-thousandths of a cent, rounded half-up to the cent
x + 5000 => y
return int((y - y % 10000) / 10000)
def run_size():
return 300
def run_quantity(k, i):
# the quantity of line i on invoice k of the month's run
lines()[i][2] => q
return 1 + (k * (i + 2) + i) % (2 * q)
def run():
# [rounded line by line, exact sum, lines rounded up, lines rounded down]
[0, 0, 0, 0] => t
1 => k
while k <= run_size():
0 => i
for ln in lines():
exact(run_quantity(k, i), ln[3]) => x
t[0] + cents(x) => t[0]
t[1] + x => t[1]
if cents(x) * 10000 > x:
t[2] + 1 => t[2]
if cents(x) * 10000 < x:
t[3] + 1 => t[3]
i + 1 => i
k + 1 => k
return t
def share(v, total):
# v of total as a whole percent, rounded half-up
v * 200 + total => y
return int((y - y % (2 * total)) / (2 * total))
def share_hundredths(v, total):
# v of total in hundredths of a percent, rounded half-up
v * 20000 + total => y
return int((y - y % (2 * total)) / (2 * total))
Python projection (invoice.py)
def tax():
return 10825
def lines():
return [["espresso beans 1 kg", "coffee", 2, 1779], ["decaf beans 1 kg", "coffee", 1, 2019], ["whole milk 2 l", "dairy", 8, 269], ["oat milk 1 l", "dairy", 4, 229], ["croissants", "bakery", 14, 139], ["muffins", "bakery", 13, 145]]
def categories():
return ["coffee", "dairy", "bakery"]
def exact(q, p):
return q * p * tax()
def cents(x):
y = x + 5000
return int((y - y % 10000) / 10000)
def run_size():
return 300
def run_quantity(k, i):
q = lines()[i][2]
return 1 + (k * (i + 2) + i) % (2 * q)
def run():
t = [0, 0, 0, 0]
k = 1
while k <= run_size():
i = 0
for ln in lines():
x = exact(run_quantity(k, i), ln[3])
t[0] = t[0] + cents(x)
t[1] = t[1] + x
if cents(x) * 10000 > x:
t[2] = t[2] + 1
if cents(x) * 10000 < x:
t[3] = t[3] + 1
i = i + 1
k = k + 1
return t
def share(v, total):
y = v * 200 + total
return int((y - y % (2 * total)) / (2 * total))
def share_hundredths(v, total):
y = v * 20000 + total
return int((y - y % (2 * total)) / (2 * total))