Money ledger
A ledger kept in whole cents beside the same sums added as floats; one amount added many times; tax on every line under four rounding rules and the drift each one leaves over many halves; and an amount shared by weights three ways, two of which always add up.
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 ledger kept in whole cents, next to the same sums added as floats. The program also adds one amount many times, works out tax on every line under four rounding rules, and shares an amount by weights so that the parts add up to it.
main.eml- the menu, the ledger table, the repeated amount, the tax table, sharing and entriesmoney.eml- amounts in cents and their text, float and exact totals, the four rounding rules, and the three ways of sharing
How each part works:
- Money is whole cents. Typed amounts have at most two decimals, and nothing is ever a float except the float column itself.
- Floats, as in the corpus case
money-float-accumulation: the same entries added as floats can miss the exact total. Ten additions of 0.01 are already off, and the float total comes out low for one count and high for the next. A report with two decimals hides the difference: it prints 0.00, or -0.00. - Tax on every line: each line's tax is an exact fraction of a cent, rounded by one of four rules: - half-up: a half away from zero; - half-even: a half to the even cent; - half-down: a half toward zero; - truncate: the fraction dropped.
The program shows the lines rounded and then added, how far that is from the exact tax, and the total rounded once. - The ledger of halves: 21 lines of 0.01, 0.03 ... 0.41. At 50% every line's tax ends in exactly half a cent. As in the corpus case rounding-rule-drift, the rules that send every half the same way drift by 10.5 cents, and half-even by half a cent. - Sharing by weights, three ways: - every part rounded on its own, which can lose or invent a cent; - the largest remainders: each part rounded down, then the cents left over go to the largest remainders, the earlier part first on a tie; - the running total rounded, each part being the difference.
The last two always add up to the amount, but can give the leftover cent to different people.
What is checked: menu choices 1 to 7; an amount with at most two decimals, up to 9,999,999.99; a description of up to 12 characters, and 30 entries at most; a tax rate from 0.01% to 100%; 2 to 8 weights, each a whole number from 1 to 9999. An empty answer cancels.
Sessions: sessions/basic.in gives: - the starting ledger, whose float total is 56.39000000000001 against an exact 56.39; - 0.01, 0.10 and 19.99 added 10, 100, 1,000 and 2,000 times; - 8.25% tax on the ledger; - the ledger of halves at 50%; - 100.00 shared 1 1 1, 0.05 shared four ways, and 10.00 shared seven ways, where rounding each part invents a cent; - an empty ledger, rent 850.00 and a deposit of -850.00.
sessions/bad-input.in gives: - menu choices 0 and x; - entries: a cancel, a description too long, amounts with three decimals, a comma and too many digits, then a cancel; - the repeated amount 0, a cancel and abc; - tax rates 0, 100.01 and abc, then a cancel; - sharing: 0, -3, one weight, a weight of 0, nine weights, and a cancel; - a new ledger of the wrong kind, then a cancel.
Built on the verified corpus cases money-float-accumulation (floats are already off at ten items, on no stable side) and rounding-rule-drift (a rule that sends every half one way drifts by half the number of ties).
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== Money ledger ==
Money kept in whole cents, next to the same sums added as floats; rounding rules for fractions of a cent; and sharing an amount so the parts add up.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 0
Pick a number from 1 to 7.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> x
Pick a number from 1 to 7.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 2
what (up to 12 letters)>
Cancelled.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 2
what (up to 12 letters)> a very long description
Keep it to 12 characters.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 2
what (up to 12 letters)> tea
amount> 12.345
Type an amount such as 12.34 or -5, at most two decimals and 9,999,999.99.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 2
what (up to 12 letters)> tea
amount> 1,50
Type an amount such as 12.34 or -5, at most two decimals and 9,999,999.99.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 2
what (up to 12 letters)> tea
amount> 10000000
Type an amount such as 12.34 or -5, at most two decimals and 9,999,999.99.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 2
what (up to 12 letters)> tea
amount>
Cancelled.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 3
amount> 0
Pick an amount that is not zero.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 3
amount>
Cancelled.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 3
amount> abc
Type an amount such as 12.34 or -5, at most two decimals and 9,999,999.99.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 4
tax rate in percent (0.01 to 100)> 0
Type a rate from 0.01 to 100, at most two decimals.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 4
tax rate in percent (0.01 to 100)> 100.01
Type a rate from 0.01 to 100, at most two decimals.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 4
tax rate in percent (0.01 to 100)> abc
Type a rate from 0.01 to 100, at most two decimals.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 4
tax rate in percent (0.01 to 100)>
Cancelled.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 5
amount to share> 0
Share an amount above zero.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 5
amount to share> -3
Share an amount above zero.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 5
amount to share> 10
weights, whole numbers such as 1 1 1 or 50 30 20> 1
Type 2 to 8 whole numbers from 1 to 9999.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 5
amount to share> 10
weights, whole numbers such as 1 1 1 or 50 30 20> 1 0 1
Type 2 to 8 whole numbers from 1 to 9999.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 5
amount to share> 10
weights, whole numbers such as 1 1 1 or 50 30 20> 1 2 3 4 5 6 7 8 9
Type 2 to 8 whole numbers from 1 to 9999.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 5
amount to share> 10
weights, whole numbers such as 1 1 1 or 50 30 20>
Cancelled.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 6
empty, or halves (21 lines of 0.01, 0.03 ... 0.41)> full
Type empty or halves.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 6
empty, or halves (21 lines of 0.01, 0.03 ... 0.41)>
Cancelled.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 7
Bye.
What was typed (53 lines)
0
x
2
2
a very long description
2
tea
12.345
2
tea
1,50
2
tea
10000000
2
tea
3
0
3
3
abc
4
0
4
100.01
4
abc
4
5
0
5
-3
5
10
1
5
10
1 0 1
5
10
1 2 3 4 5 6 7 8 9
5
10
6
full
6
7
basic
interpreter: byte-equal== Money ledger ==
Money kept in whole cents, next to the same sums added as floats; rounding rules for fractions of a cent; and sharing an amount so the parts add up.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 1
entry amount running total
coffee 3.10 3.10
bus 2.70 5.80
lunch 12.60 18.40
refund -5.00 13.40
books 39.99 53.39
snack 1.10 54.49
tip 0.70 55.19
milk 1.20 56.39
In whole cents: 56.39. Added as floats: 56.39000000000001.
They differ by 7.105427357601002e-15 - a report with two decimals prints that difference as 0.00.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 3
amount> 0.01
times as floats exact equal
10 0.09999999999999999 0.10 no, low
100 1.0000000000000007 1.00 no, high
1000 9.999999999999831 10.00 no, low
2000 20.000000000000327 20.00 no, high
Ten additions can already be off, and the float total can come out low for one count and high for the next.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 3
amount> 0.10
times as floats exact equal
10 0.9999999999999999 1.00 no, low
100 9.99999999999998 10.00 no, low
1000 99.9999999999986 100.00 no, low
2000 199.99999999999292 200.00 no, low
Ten additions can already be off, and the float total can come out low for one count and high for the next.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 3
amount> 19.99
times as floats exact equal
10 199.9 199.90 yes
100 1999.0000000000007 1,999.00 no, high
1000 19990.000000000135 19,990.00 no, high
2000 39980.00000000042 39,980.00 no, high
Ten additions can already be off, and the float total can come out low for one count and high for the next.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 4
tax rate in percent (0.01 to 100)> 8.25
8.25% on every line: the exact tax is 4.652175; 0 of the 8 lines land exactly on half a cent.
rule lines rounded, then added off by the total rounded
half-up 4.66 +0.007825 4.65
half-even 4.66 +0.007825 4.65
half-down 4.66 +0.007825 4.65
truncate 4.61 -0.042175 4.65
half-up sends every half a cent away from zero, half-down toward it, and truncate drops the fraction. half-even goes to the even cent, so over many lines its halves tend to cancel out.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 6
empty, or halves (21 lines of 0.01, 0.03 ... 0.41)> halves
21 lines of odd cents: at 50% each one's tax is exactly half a cent over a whole one.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 4
tax rate in percent (0.01 to 100)> 50
50.00% on every line: the exact tax is 2.205000; 21 of the 21 lines land exactly on half a cent.
rule lines rounded, then added off by the total rounded
half-up 2.31 +0.105000 2.21
half-even 2.20 -0.005000 2.20
half-down 2.10 -0.105000 2.20
truncate 2.10 -0.105000 2.20
half-up sends every half a cent away from zero, half-down toward it, and truncate drops the fraction. half-even goes to the even cent, so over many lines its halves tend to cancel out.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 5
amount to share> 100
weights, whole numbers such as 1 1 1 or 50 30 20> 1 1 1
Sharing 100.00 by 1 1 1:
each rounded 33.33 33.33 33.33 adds up to 99.99, not 100.00
largest remainder 33.34 33.33 33.33
running total 33.33 33.34 33.33
Rounding every part on its own can lose or invent cents. The largest remainders, or the rounded running total, always add up; they can give a leftover cent to different people.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 5
amount to share> 0.05
weights, whole numbers such as 1 1 1 or 50 30 20> 1 1 1 1
Sharing 0.05 by 1 1 1 1:
each rounded 0.01 0.01 0.01 0.01 adds up to 0.04, not 0.05
largest remainder 0.02 0.01 0.01 0.01
running total 0.01 0.02 0.01 0.01
Rounding every part on its own can lose or invent cents. The largest remainders, or the rounded running total, always add up; they can give a leftover cent to different people.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 5
amount to share> 10
weights, whole numbers such as 1 1 1 or 50 30 20> 1 1 1 1 1 1 1
Sharing 10.00 by 1 1 1 1 1 1 1:
each rounded 1.43 1.43 1.43 1.43 1.43 1.43 1.43 adds up to 10.01, not 10.00
largest remainder 1.43 1.43 1.43 1.43 1.43 1.43 1.42
running total 1.43 1.43 1.43 1.42 1.43 1.43 1.43
Rounding every part on its own can lose or invent cents. The largest remainders, or the rounded running total, always add up; they can give a leftover cent to different people.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 6
empty, or halves (21 lines of 0.01, 0.03 ... 0.41)> empty
The ledger is empty.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 2
what (up to 12 letters)> rent
amount> 850
rent 850.00 added.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 2
what (up to 12 letters)> deposit
amount> -850
deposit -850.00 added.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 1
entry amount running total
rent 850.00 850.00
deposit -850.00 0.00
In whole cents: 0.00. Added as floats: 0.0.
The two agree this time.
1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit
choice> 7
Bye.
What was typed (32 lines)
1
3
0.01
3
0.10
3
19.99
4
8.25
6
halves
4
50
5
100
1 1 1
5
0.05
1 1 1 1
5
10
1 1 1 1 1 1 1
6
empty
2
rent
850
2
deposit
-850
1
7
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# P104 money ledger: a ledger kept in whole cents beside the same sums in
# floats, one amount added many times, tax on every line under four
# rounding rules, and an amount shared by weights.
import money
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 number(s, most):
if len(s) == 0 or len(s) > most:
return -1
0 => n
for c in s:
if not (c >= "0" and c <= "9"):
return -1
n * 10 + int(c) => n
return n
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_amount(prompt):
# [cents], [] to cancel, or ["!"] after a complaint
plain(input(prompt + "> ")) => t
if t == "":
return []
money.parse_amount(t) => a
if len(a) == 0:
"Type an amount such as 12.34 or -5, at most two decimals and 9,999,999.99." ^0
return ["!"]
return a
def show_ledger(entries):
if len(entries) == 0:
"The ledger is empty." ^0
return
" entry amount running total" ^0
0 => run
for e in entries:
run + e[1] => run
(" " + pad(e[0], 12) + lpad(money.cents_text(e[1]), 9) + lpad(money.cents_text(run), 18)) ^0
money.float_total(entries) => f
money.exact_total(entries) => x
f - x / 100.0 => d
("In whole cents: " + money.cents_text(x) + ". Added as floats: " + str(f) + ".") ^0
if d == 0.0:
"The two agree this time." ^0
else:
("They differ by " + str(d) + " - a report with two decimals prints that difference as " + money.float_cents_text(d) + ".") ^0
def many():
ask_amount("amount") => a
if len(a) == 0:
"Cancelled." ^0
return
if a[0] == "!":
return
if a[0] == 0:
"Pick an amount that is not zero." ^0
return
(" times as floats exact equal") ^0
for n in [10, 100, 1000, 2000]:
money.repeat(a[0], n) => r
"yes" => eq
if r[0] != r[1] / 100.0:
"no, low" => eq
if r[0] > r[1] / 100.0:
"no, high" => eq
(" " + lpad(str(n), 5) + " " + pad(str(r[0]), 22) + lpad(money.cents_text(r[1]), 13) + " " + eq) ^0
"Ten additions can already be off, and the float total can come out low for one count and high for the next." ^0
def micro(num):
# num / 10000 cents, written as an amount with six decimals
"" => sign
num => a
if num < 0:
"-" => sign
0 - num => a
a % 10000 => f
int((a - f) / 10000) => c
str(f) => fs
while len(fs) < 4:
"0" + fs => fs
money.cents_text(c) => ct
return sign + ct + fs
def tax(entries):
if len(entries) == 0:
"The ledger is empty." ^0
return
plain(input("tax rate in percent (0.01 to 100)> ")) => t
if t == "":
"Cancelled." ^0
return
money.parse_amount(t) => r
if len(r) == 0 or r[0] < 1 or r[0] > 10000:
"Type a rate from 0.01 to 100, at most two decimals." ^0
return
r[0] => bp
0 => ties
for e in entries:
if money.is_tie(e[1] * bp, 10000):
ties + 1 => ties
money.exact_total(entries) * bp => exact
" lines land" => land
if ties == 1:
" line lands" => land
(money.cents_text(bp) + "% on every line: the exact tax is " + micro(exact) + "; " + str(ties) + " of the " + str(len(entries)) + land + " exactly on half a cent.") ^0
" rule lines rounded, then added off by the total rounded" ^0
for rule in ["half-up", "half-even", "half-down", "truncate"]:
0 => s
for e in entries:
s + money.round_div(e[1] * bp, 10000, rule) => s
money.round_div(exact, 10000, rule) => once
micro(s * 10000 - exact) => off
if s * 10000 - exact > 0:
"+" + off => off
(" " + pad(rule, 12) + lpad(money.cents_text(s), 12) + " " + pad(off, 13) + lpad(money.cents_text(once), 10)) ^0
"half-up sends every half a cent away from zero, half-down toward it, and truncate drops the fraction. half-even goes to the even cent, so over many lines its halves tend to cancel out." ^0
def share_menu():
ask_amount("amount to share") => a
if len(a) == 0:
"Cancelled." ^0
return
if a[0] == "!":
return
if a[0] <= 0:
"Share an amount above zero." ^0
return
plain(input("weights, whole numbers such as 1 1 1 or 50 30 20> ")) => t
if t == "":
"Cancelled." ^0
return
[] => ws
"" => w
True => ok
for c in t + " ":
if c == " ":
if w != "":
number(w, 4) => v
if v < 1:
False => ok
ws + [v] => ws
"" => w
else:
w + c => w
if not ok or len(ws) < 2 or len(ws) > 8:
"Type 2 to 8 whole numbers from 1 to 9999." ^0
return
("Sharing " + money.cents_text(a[0]) + " by " + t + ":") ^0
["each", "largest", "running"] => methods
["each rounded", "largest remainder", "running total"] => names
for k in [0:2]:
money.share(a[0], ws, methods[k]) => parts
"" => line
0 => sum
for p in parts:
if line != "":
line + " " => line
line + money.cents_text(p) => line
sum + p => sum
"" => note
if sum != a[0]:
" adds up to " + money.cents_text(sum) + ", not " + money.cents_text(a[0]) => note
(" " + pad(names[k], 18) + line + note) ^0
"Rounding every part on its own can lose or invent cents. The largest remainders, or the rounded running total, always add up; they can give a leftover cent to different people." ^0
def add(entries):
plain(input("what (up to 12 letters)> ")) => d
if d == "":
"Cancelled." ^0
return entries
if len(d) > 12:
"Keep it to 12 characters." ^0
return entries
ask_amount("amount") => a
if len(a) == 0:
"Cancelled." ^0
return entries
if a[0] == "!":
return entries
if len(entries) >= 30:
"The ledger holds 30 entries." ^0
return entries
(d + " " + money.cents_text(a[0]) + " added.") ^0
return entries + [[d, a[0]]]
[["coffee", 310], ["bus", 270], ["lunch", 1260], ["refund", 0 - 500], ["books", 3999], ["snack", 110], ["tip", 70], ["milk", 120]] => entries
"== Money ledger ==" ^0
"Money kept in whole cents, next to the same sums added as floats; rounding rules for fractions of a cent; and sharing an amount so the parts add up." ^0
True => running
while running:
"" ^0
"1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit" ^0
plain(input("choice> ")) => choice
if choice == "1":
show_ledger(entries)
elif choice == "2":
add(entries) => entries
elif choice == "3":
many()
elif choice == "4":
tax(entries)
elif choice == "5":
share_menu()
elif choice == "6":
plain(input("empty, or halves (21 lines of 0.01, 0.03 ... 0.41)> ")) => k
if k == "empty":
[] => entries
"The ledger is empty." ^0
elif k == "halves":
[] => entries
for i in [0:20]:
entries + [["line " + str(i + 1), 2 * i + 1]] => entries
"21 lines of odd cents: at 50% each one's tax is exactly half a cent over a whole one." ^0
elif k == "":
"Cancelled." ^0
else:
"Type empty or halves." ^0
elif choice == "7":
False => running
else:
"Pick a number from 1 to 7." ^0
"Bye." ^0
Python projection (main.py)
import money
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 number(s, most):
if len(s) == 0 or len(s) > most:
return -1
n = 0
for c in s:
if not (c >= "0" and c <= "9"):
return -1
n = n * 10 + int(c)
return n
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_amount(prompt):
t = plain(input(prompt + "> "))
if t == "":
return []
a = money.parse_amount(t)
if len(a) == 0:
print("Type an amount such as 12.34 or -5, at most two decimals and 9,999,999.99.")
return ["!"]
return a
def show_ledger(entries):
if len(entries) == 0:
print("The ledger is empty.")
return
print(" entry amount running total")
run = 0
for e in entries:
run = run + e[1]
print(" " + pad(e[0], 12) + lpad(money.cents_text(e[1]), 9) + lpad(money.cents_text(run), 18))
f = money.float_total(entries)
x = money.exact_total(entries)
d = f - x / 100.0
print("In whole cents: " + money.cents_text(x) + ". Added as floats: " + str(f) + ".")
if d == 0.0:
print("The two agree this time.")
else:
print("They differ by " + str(d) + " - a report with two decimals prints that difference as " + money.float_cents_text(d) + ".")
def many():
a = ask_amount("amount")
if len(a) == 0:
print("Cancelled.")
return
if a[0] == "!":
return
if a[0] == 0:
print("Pick an amount that is not zero.")
return
print(" times as floats exact equal")
for n in [10, 100, 1000, 2000]:
r = money.repeat(a[0], n)
eq = "yes"
if r[0] != r[1] / 100.0:
eq = "no, low"
if r[0] > r[1] / 100.0:
eq = "no, high"
print(" " + lpad(str(n), 5) + " " + pad(str(r[0]), 22) + lpad(money.cents_text(r[1]), 13) + " " + eq)
print("Ten additions can already be off, and the float total can come out low for one count and high for the next.")
def micro(num):
sign = ""
a = num
if num < 0:
sign = "-"
a = 0 - num
f = a % 10000
c = int((a - f) / 10000)
fs = str(f)
while len(fs) < 4:
fs = "0" + fs
ct = money.cents_text(c)
return sign + ct + fs
def tax(entries):
if len(entries) == 0:
print("The ledger is empty.")
return
t = plain(input("tax rate in percent (0.01 to 100)> "))
if t == "":
print("Cancelled.")
return
r = money.parse_amount(t)
if len(r) == 0 or r[0] < 1 or r[0] > 10000:
print("Type a rate from 0.01 to 100, at most two decimals.")
return
bp = r[0]
ties = 0
for e in entries:
if money.is_tie(e[1] * bp, 10000):
ties = ties + 1
exact = money.exact_total(entries) * bp
land = " lines land"
if ties == 1:
land = " line lands"
print(money.cents_text(bp) + "% on every line: the exact tax is " + micro(exact) + "; " + str(ties) + " of the " + str(len(entries)) + land + " exactly on half a cent.")
print(" rule lines rounded, then added off by the total rounded")
for rule in ["half-up", "half-even", "half-down", "truncate"]:
s = 0
for e in entries:
s = s + money.round_div(e[1] * bp, 10000, rule)
once = money.round_div(exact, 10000, rule)
off = micro(s * 10000 - exact)
if s * 10000 - exact > 0:
off = "+" + off
print(" " + pad(rule, 12) + lpad(money.cents_text(s), 12) + " " + pad(off, 13) + lpad(money.cents_text(once), 10))
print("half-up sends every half a cent away from zero, half-down toward it, and truncate drops the fraction. half-even goes to the even cent, so over many lines its halves tend to cancel out.")
def share_menu():
a = ask_amount("amount to share")
if len(a) == 0:
print("Cancelled.")
return
if a[0] == "!":
return
if a[0] <= 0:
print("Share an amount above zero.")
return
t = plain(input("weights, whole numbers such as 1 1 1 or 50 30 20> "))
if t == "":
print("Cancelled.")
return
ws = []
w = ""
ok = True
for c in t + " ":
if c == " ":
if w != "":
v = number(w, 4)
if v < 1:
ok = False
ws = ws + [v]
w = ""
else:
w = w + c
if not ok or len(ws) < 2 or len(ws) > 8:
print("Type 2 to 8 whole numbers from 1 to 9999.")
return
print("Sharing " + money.cents_text(a[0]) + " by " + t + ":")
methods = ["each", "largest", "running"]
names = ["each rounded", "largest remainder", "running total"]
for k in range(0, 3):
parts = money.share(a[0], ws, methods[k])
line = ""
sum = 0
for p in parts:
if line != "":
line = line + " "
line = line + money.cents_text(p)
sum = sum + p
note = ""
if sum != a[0]:
note = " adds up to " + money.cents_text(sum) + ", not " + money.cents_text(a[0])
print(" " + pad(names[k], 18) + line + note)
print("Rounding every part on its own can lose or invent cents. The largest remainders, or the rounded running total, always add up; they can give a leftover cent to different people.")
def add(entries):
d = plain(input("what (up to 12 letters)> "))
if d == "":
print("Cancelled.")
return entries
if len(d) > 12:
print("Keep it to 12 characters.")
return entries
a = ask_amount("amount")
if len(a) == 0:
print("Cancelled.")
return entries
if a[0] == "!":
return entries
if len(entries) >= 30:
print("The ledger holds 30 entries.")
return entries
print(d + " " + money.cents_text(a[0]) + " added.")
return entries + [[d, a[0]]]
entries = [["coffee", 310], ["bus", 270], ["lunch", 1260], ["refund", 0 - 500], ["books", 3999], ["snack", 110], ["tip", 70], ["milk", 120]]
print("== Money ledger ==")
print("Money kept in whole cents, next to the same sums added as floats; rounding rules for fractions of a cent; and sharing an amount so the parts add up.")
running = True
while running:
print("")
print("1) ledger 2) add an entry 3) one amount many times 4) tax on every line 5) share an amount 6) new ledger 7) quit")
choice = plain(input("choice> "))
if choice == "1":
show_ledger(entries)
elif choice == "2":
entries = add(entries)
elif choice == "3":
many()
elif choice == "4":
tax(entries)
elif choice == "5":
share_menu()
elif choice == "6":
k = plain(input("empty, or halves (21 lines of 0.01, 0.03 ... 0.41)> "))
if k == "empty":
entries = []
print("The ledger is empty.")
elif k == "halves":
entries = []
for i in range(0, 21):
entries = entries + [["line " + str(i + 1), 2 * i + 1]]
print("21 lines of odd cents: at 50% each one's tax is exactly half a cent over a whole one.")
elif k == "":
print("Cancelled.")
else:
print("Type empty or halves.")
elif choice == "7":
running = False
else:
print("Pick a number from 1 to 7.")
print("Bye.")
money.eml
eml# P104 money ledger - money in whole cents, beside the same sums in floats
# (the corpus case money-float-accumulation: ten items are already off,
# and the side the error falls on is not stable); fractions of a cent
# rounded by four rules, whose drift over many ties is the corpus case
# rounding-rule-drift; and an amount shared by weights so that the parts
# add up to it exactly.
def cents_text(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 parse_amount(t):
# "12.34", "-5", "0.5" -> cents, or [] if not a sum of money with at
# most two decimals and at most 9,999,999.99
if len(t) == 0:
return []
1 => sign
t => s
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) > 7:
return []
while len(frac) < 2:
frac + "0" => frac
return [sign * (int(whole) * 100 + int(frac))]
def float_total(entries):
0.0 => t
for e in entries:
t + e[1] / 100.0 => t
return t
def exact_total(entries):
0 => t
for e in entries:
t + e[1] => t
return t
def float_cents_text(x):
# how a report with two decimals shows a float: the cents nearest it,
# and "-0.00" for a tiny negative difference, as Python's %.2f does
x * 100.0 => y
"" => sign
if y < 0:
"-" => sign
0.0 - y => y
int(y + 0.5) => c
if sign == "-" and c == 0:
return "-0.00"
if sign == "-":
return cents_text(0 - c)
return cents_text(c)
def repeat(amount, n):
# [float total, exact cents] of n times the same amount
0.0 => t
for i in [1:n]:
t + amount / 100.0 => t
return [t, amount * n]
def round_div(num, den, rule):
# num / den (den > 0) to a whole number by rule: "half-up" (half away
# from zero), "half-even", "half-down" (half toward zero), "truncate"
0 => neg
num => a
if num < 0:
1 => neg
0 - num => a
a % den => r
int((a - r) / den) => q
if rule == "truncate":
0 => up
elif 2 * r > den:
1 => up
elif 2 * r < den:
0 => up
elif rule == "half-up":
1 => up
elif rule == "half-down":
0 => up
else:
q % 2 => up
q + up => q
if neg == 1:
return 0 - q
return q
def is_tie(num, den):
0 => a
num => a
if num < 0:
0 - num => a
return 2 * (a % den) == den
def share(total, weights, method):
# parts that add up to total, by weight; "each" rounds every part on
# its own (half-up) and may not add up; "largest" gives the cents left
# over to the largest remainders; "running" rounds the running sum
0 => w
for x in weights:
w + x => w
[] => out
if method == "each":
for x in weights:
out + [round_div(total * x, w, "half-up")] => out
return out
if method == "running":
0 => run
0 => before
for x in weights:
run + x => run
round_div(total * run, w, "half-up") => now
out + [now - before] => out
now => before
return out
# largest remainder: floors first, then one cent each to the largest
# remainders, earlier first on ties
[] => rems
0 => given
for x in weights:
total * x => num
num % w => r
int((num - r) / w) => f
out + [f] => out
rems + [r] => rems
given + f => given
total - given => left
[] => used
for x in weights:
used + [False] => used
0 => k
while k < left:
-1 => best
for i in [0:len(weights) - 1]:
if not used[i] and (best == -1 or rems[i] > rems[best]):
i => best
True => used[best]
out[best] + 1 => out[best]
k + 1 => k
return out
Python projection (money.py)
def cents_text(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 parse_amount(t):
if len(t) == 0:
return []
sign = 1
s = t
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) > 7:
return []
while len(frac) < 2:
frac = frac + "0"
return [sign * (int(whole) * 100 + int(frac))]
def float_total(entries):
t = 0.0
for e in entries:
t = t + e[1] / 100.0
return t
def exact_total(entries):
t = 0
for e in entries:
t = t + e[1]
return t
def float_cents_text(x):
y = x * 100.0
sign = ""
if y < 0:
sign = "-"
y = 0.0 - y
c = int(y + 0.5)
if sign == "-" and c == 0:
return "-0.00"
if sign == "-":
return cents_text(0 - c)
return cents_text(c)
def repeat(amount, n):
t = 0.0
for i in range(1, n+1):
t = t + amount / 100.0
return [t, amount * n]
def round_div(num, den, rule):
neg = 0
a = num
if num < 0:
neg = 1
a = 0 - num
r = a % den
q = int((a - r) / den)
if rule == "truncate":
up = 0
elif 2 * r > den:
up = 1
elif 2 * r < den:
up = 0
elif rule == "half-up":
up = 1
elif rule == "half-down":
up = 0
else:
up = q % 2
q = q + up
if neg == 1:
return 0 - q
return q
def is_tie(num, den):
a = 0
a = num
if num < 0:
a = 0 - num
return 2 * (a % den) == den
def share(total, weights, method):
w = 0
for x in weights:
w = w + x
out = []
if method == "each":
for x in weights:
out = out + [round_div(total * x, w, "half-up")]
return out
if method == "running":
run = 0
before = 0
for x in weights:
run = run + x
now = round_div(total * run, w, "half-up")
out = out + [now - before]
before = now
return out
rems = []
given = 0
for x in weights:
num = total * x
r = num % w
f = int((num - r) / w)
out = out + [f]
rems = rems + [r]
given = given + f
left = total - given
used = []
for x in weights:
used = used + [False]
k = 0
while k < left:
best = -1
for i in range(0, len(weights)):
if not used[i] and (best == -1 or rems[i] > rems[best]):
best = i
used[best] = True
out[best] = out[best] + 1
k = k + 1
return out