Savings ledger
A savings account in whole cents: deposits, withdrawals that are refused rather than allowed to take it below zero, monthly compound interest rounded to the cent, a statement, a check of the books worked out from the log alone, and how many months a savings goal would take.
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 savings account kept in whole cents. You can deposit, and withdraw - a withdrawal that would take the balance below zero is refused. Each month ends with compound interest, rounded to the cent. The statement shows every entry with its running balance. A check of the books works the balance out again from the log alone, and a goal says how many months a savings target would take.
main.eml- the menu, reading amounts and the rate, and the statement, books and goal messagesledger.eml- the account, interest, money formatting, the books check and the goal
How each part works:
- Money is whole cents, and the yearly rate is in hundredths of a percent: 2.50% is 250. A month's interest is balance x rate / 10000 / 12, rounded to the nearest cent, halves up, and is added to the balance, so the next month earns interest on it too.
- The account is a class. A deposit or withdrawal that breaks the rules raises an error instead of returning a status that could be ignored, as in the corpus cases
bank-account-simulatorandbank-account-invariant. The menu catches it and says why. A refused withdrawal is written in the log but changes nothing. - Checking the books follows the second corpus case. It adds the deposits, withdrawals and interest from the log, without asking the account, and checks three things: the total equals the balance, every running balance on the way was at least 0.00, and every entry's balance equals the one before it plus its own change.
- A goal puts the monthly amount in at the start of each month and earns that month's interest at its end, until the target is reached. It gives up after 100 years.
What is checked: menu choices 1 to 8; amounts as whole numbers or with up to two decimals, commas between thousands allowed, up to 1,000,000.00; deposits and withdrawals above 0.00; rates from 0 to 20%. An empty answer cancels.
Sessions: sessions/basic.in gives: - deposits of 1,000 and 250.50; - a month at 2.50%, which earns 2.61 (1,250.50 x 2.5% / 12 = 2.6052); - a withdrawal of 300, then two more months; - the statement, and the books agreeing at 957.09; - the months to 10,000 at 500 a month: 18 at 2.50%, and again 18 at 4.00%, with 349.56 of interest instead of 216.79.
sessions/bad-input.in gives: - menu choices 0 and x; - a withdrawal from the empty account; - amounts abc, -5, 12.345 and 1000000.01, and a deposit of 0; - 1,500 with a comma, then a withdrawal of 2,000, which is refused; - rates 25 and x; - a goal below the balance; - a cancelled goal; - at 0%, 1,000,000 at 1.00 a month, which is more than 100 years; - the books with two refused withdrawals, and the statement.
Built on the verified corpus cases bank-account-simulator (an account class that raises on a bad withdrawal) and bank-account-invariant (the balance never negative, checked again from the log of allowed operations).
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== Savings ledger ==
Whole cents, monthly interest, and a balance that never goes below zero.
The account opens in 2026-10 at 2.50% a year, paid monthly.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 0
Pick a number from 1 to 8.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> x
Pick a number from 1 to 8.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 2
withdraw> 50
Refused: only 0.00 is in the account; the balance stays 0.00.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 1
deposit> abc
Type an amount like 250 or 12.50, up to 1,000,000.00.
deposit> -5
Type an amount like 250 or 12.50, up to 1,000,000.00.
deposit> 12.345
Type an amount like 250 or 12.50, up to 1,000,000.00.
deposit> 1000000.01
Type an amount like 250 or 12.50, up to 1,000,000.00.
deposit> 0
Not done: a deposit must be more than 0.00.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 1
deposit> 1,500
Deposited 1,500.00 in 2026-10. Balance 1,500.00.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 2
withdraw> 2000
Refused: only 1,500.00 is in the account; the balance stays 1,500.00.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 7
yearly rate in % (0 to 20)> 25
Type a rate like 2.5 or 3.25, from 0 to 20.
yearly rate in % (0 to 20)> x
Type a rate like 2.5 or 3.25, from 0 to 20.
yearly rate in % (0 to 20)>
Cancelled.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 6
goal> 100
The balance, 1,500.00, is there already.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 6
goal> 5000
put in each month>
Cancelled.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 7
yearly rate in % (0 to 20)> 0
The rate is now 0.00% a year, paid monthly.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 6
goal> 1,000,000
put in each month> 1
Putting in 1.00 a month, it would take more than 100 years.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 5
From the log: deposits 1,500.00, withdrawals 0.00, interest 0.00.
They add up to 1,500.00; the account says 1,500.00.
The books agree, every balance on the way was at least 0.00, and 2 refused withdrawal(s) changed nothing.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 4
month entry amount balance
2026-10 refused (50.00) 0.00
2026-10 deposit 1,500.00 1,500.00
2026-10 refused (2,000.00) 1,500.00
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 8
Bye.
What was typed (31 lines)
0
x
2
50
1
abc
-5
12.345
1000000.01
0
1
1,500
2
2000
7
25
x
6
100
6
5000
7
0
6
1,000,000
1
5
4
8
basic
interpreter: byte-equal== Savings ledger ==
Whole cents, monthly interest, and a balance that never goes below zero.
The account opens in 2026-10 at 2.50% a year, paid monthly.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 1
deposit> 1000
Deposited 1,000.00 in 2026-10. Balance 1,000.00.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 1
deposit> 250.50
Deposited 250.50 in 2026-10. Balance 1,250.50.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 3
2026-10 closes: interest 2.61 at 2.50% a year. Balance 1,253.11; now 2026-11.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 2
withdraw> 300
Withdrew 300.00. Balance 953.11.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 3
2026-11 closes: interest 1.99 at 2.50% a year. Balance 955.10; now 2026-12.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 3
2026-12 closes: interest 1.99 at 2.50% a year. Balance 957.09; now 2027-01.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 4
month entry amount balance
2026-10 deposit 1,000.00 1,000.00
2026-10 deposit 250.50 1,250.50
2026-10 interest 2.61 1,253.11
2026-11 withdraw -300.00 953.11
2026-11 interest 1.99 955.10
2026-12 interest 1.99 957.09
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 5
From the log: deposits 1,250.50, withdrawals 300.00, interest 6.59.
They add up to 957.09; the account says 957.09.
The books agree, every balance on the way was at least 0.00, and 0 refused withdrawal(s) changed nothing.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 6
goal> 10,000
put in each month> 500
Putting in 500.00 a month at 2.50%: 18 months (1 year 6 months), reaching 10,173.88 with 216.79 of it interest.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 7
yearly rate in % (0 to 20)> 4
The rate is now 4.00% a year, paid monthly.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 6
goal> 10,000
put in each month> 500
Putting in 500.00 a month at 4.00%: 18 months (1 year 6 months), reaching 10,306.65 with 349.56 of it interest.
1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit
choice> 8
Bye.
What was typed (20 lines)
1
1000
1
250.50
3
2
300
3
3
4
5
6
10,000
500
7
4
6
10,000
500
8
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# P064 savings ledger: a savings account in whole cents - deposits,
# withdrawals that can never take it below zero, monthly compound interest
# - with a statement, a check of the books from the log alone, and how long
# a goal would take.
import ledger
def trim(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 cents_of(s, places):
# "120", "120.5", "1,200.50" as a whole number in 1/10^places, or -1.
# Commas between the thousands are skipped.
"" => t
for ch in s:
if ch != ",":
t + ch => t
0 => k
while k < len(t) and t[k] != ".":
k + 1 => k
t[0:k] => whole
"" => part
if k < len(t):
t[k + 1:len(t)] => part
if whole == "" or len(whole) > 7 or len(part) > places:
return -1
for ch in whole + part:
if not (ch >= "0" and ch <= "9"):
return -1
while len(part) < places:
part + "0" => part
0 => n
for ch in whole + part:
n * 10 + int(ch) => n
return n
def month_label(m):
# Month 0 is 2026-10.
9 + m => k
2026 + ledger.quotient(k, 12) => y
str(k % 12 + 1) => mm
if len(mm) == 1:
"0" + mm => mm
return str(y) + "-" + mm
def rate_text(rate):
str(rate % 100) => part
if len(part) == 1:
"0" + part => part
return str(ledger.quotient(rate, 100)) + "." + part + "%"
def ask_amount(prompt):
while True:
trim(input(prompt)) => answer
if answer == "":
return -1
cents_of(answer, 2) => c
if c >= 0 and c <= 100000000:
return c
"Type an amount like 250 or 12.50, up to 1,000,000.00." ^0
def deposit(acct):
ask_amount("deposit> ") => c
if c == -1:
"Cancelled." ^0
return
try:
acct.deposit(c)
("Deposited " + ledger.money(c) + " in " + month_label(acct.month) + ". Balance " + ledger.money(acct.balance) + ".") ^0
except ValueError as e:
("Not done: " + str(e) + ".") ^0
def withdraw(acct):
ask_amount("withdraw> ") => c
if c == -1:
"Cancelled." ^0
return
try:
acct.withdraw(c)
("Withdrew " + ledger.money(c) + ". Balance " + ledger.money(acct.balance) + ".") ^0
except ValueError as e:
("Refused: " + str(e) + "; the balance stays " + ledger.money(acct.balance) + ".") ^0
def next_month(acct):
month_label(acct.month) => ending
acct.next_month() => earned
(ending + " closes: interest " + ledger.money(earned) + " at " + rate_text(acct.rate) + " a year. Balance " + ledger.money(acct.balance) + "; now " + month_label(acct.month) + ".") ^0
def counted(n, one, many):
if n == 1:
return "1 " + one
return str(n) + " " + many
def padded(s, width):
while len(s) < width:
" " + s => s
return s
def statement(acct):
if len(acct.log) == 0:
"Nothing has happened in the account yet." ^0
return
"month entry amount balance" ^0
for e in acct.log:
e[1] => kind
while len(kind) < 9:
kind + " " => kind
ledger.money(e[2]) => amount
if e[1] == "withdraw":
"-" + amount => amount
elif e[1] == "refused":
"(" + amount + ")" => amount
(month_label(e[0]) + " " + kind + padded(amount, 13) + padded(ledger.money(e[3]), 14)) ^0
def check_books(acct):
ledger.books(acct.log) => b
("From the log: deposits " + ledger.money(b[0]) + ", withdrawals " + ledger.money(b[1]) + ", interest " + ledger.money(b[2]) + ".") ^0
("They add up to " + ledger.money(b[3]) + "; the account says " + ledger.money(acct.balance) + ".") ^0
0 => refused
for e in acct.log:
if e[1] == "refused":
refused + 1 => refused
if b[3] == acct.balance and b[4] and b[5]:
("The books agree, every balance on the way was at least 0.00, and " + str(refused) + " refused withdrawal(s) changed nothing.") ^0
else:
"The books do NOT agree." ^0
def goal(acct):
ask_amount("goal> ") => g
if g == -1:
"Cancelled." ^0
return
if g <= acct.balance:
("The balance, " + ledger.money(acct.balance) + ", is there already.") ^0
return
ask_amount("put in each month> ") => m
if m == -1:
"Cancelled." ^0
return
ledger.months_to_goal(acct.balance, acct.rate, m, g) => r
if r[0] == -1:
("Putting in " + ledger.money(m) + " a month, it would take more than 100 years.") ^0
return
ledger.quotient(r[0], 12) => years
("Putting in " + ledger.money(m) + " a month at " + rate_text(acct.rate) + ": " + counted(r[0], "month", "months") + " (" + counted(years, "year", "years") + " " + counted(r[0] % 12, "month", "months") + "), reaching " + ledger.money(r[1]) + " with " + ledger.money(r[2]) + " of it interest.") ^0
def set_rate(acct):
while True:
trim(input("yearly rate in % (0 to 20)> ")) => answer
if answer == "":
"Cancelled." ^0
return
cents_of(answer, 2) => r
if r >= 0 and r <= 2000:
r => acct.rate
("The rate is now " + rate_text(r) + " a year, paid monthly.") ^0
return
"Type a rate like 2.5 or 3.25, from 0 to 20." ^0
"== Savings ledger ==" ^0
"Whole cents, monthly interest, and a balance that never goes below zero." ^0
ledger.Account(250) => acct
("The account opens in " + month_label(0) + " at " + rate_text(250) + " a year, paid monthly.") ^0
True => running
while running:
"" ^0
"1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit" ^0
trim(input("choice> ")) => choice
if choice == "1":
deposit(acct)
elif choice == "2":
withdraw(acct)
elif choice == "3":
next_month(acct)
elif choice == "4":
statement(acct)
elif choice == "5":
check_books(acct)
elif choice == "6":
goal(acct)
elif choice == "7":
set_rate(acct)
elif choice == "8":
False => running
else:
"Pick a number from 1 to 8." ^0
"Bye." ^0
Python projection (main.py)
import ledger
def trim(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 cents_of(s, places):
t = ""
for ch in s:
if ch != ",":
t = t + ch
k = 0
while k < len(t) and t[k] != ".":
k = k + 1
whole = t[0:k]
part = ""
if k < len(t):
part = t[k + 1:len(t)]
if whole == "" or len(whole) > 7 or len(part) > places:
return -1
for ch in whole + part:
if not (ch >= "0" and ch <= "9"):
return -1
while len(part) < places:
part = part + "0"
n = 0
for ch in whole + part:
n = n * 10 + int(ch)
return n
def month_label(m):
k = 9 + m
y = 2026 + ledger.quotient(k, 12)
mm = str(k % 12 + 1)
if len(mm) == 1:
mm = "0" + mm
return str(y) + "-" + mm
def rate_text(rate):
part = str(rate % 100)
if len(part) == 1:
part = "0" + part
return str(ledger.quotient(rate, 100)) + "." + part + "%"
def ask_amount(prompt):
while True:
answer = trim(input(prompt))
if answer == "":
return -1
c = cents_of(answer, 2)
if c >= 0 and c <= 100000000:
return c
print("Type an amount like 250 or 12.50, up to 1,000,000.00.")
def deposit(acct):
c = ask_amount("deposit> ")
if c == -1:
print("Cancelled.")
return
try:
acct.deposit(c)
print("Deposited " + ledger.money(c) + " in " + month_label(acct.month) + ". Balance " + ledger.money(acct.balance) + ".")
except ValueError as e:
print("Not done: " + str(e) + ".")
def withdraw(acct):
c = ask_amount("withdraw> ")
if c == -1:
print("Cancelled.")
return
try:
acct.withdraw(c)
print("Withdrew " + ledger.money(c) + ". Balance " + ledger.money(acct.balance) + ".")
except ValueError as e:
print("Refused: " + str(e) + "; the balance stays " + ledger.money(acct.balance) + ".")
def next_month(acct):
ending = month_label(acct.month)
earned = acct.next_month()
print(ending + " closes: interest " + ledger.money(earned) + " at " + rate_text(acct.rate) + " a year. Balance " + ledger.money(acct.balance) + "; now " + month_label(acct.month) + ".")
def counted(n, one, many):
if n == 1:
return "1 " + one
return str(n) + " " + many
def padded(s, width):
while len(s) < width:
s = " " + s
return s
def statement(acct):
if len(acct.log) == 0:
print("Nothing has happened in the account yet.")
return
print("month entry amount balance")
for e in acct.log:
kind = e[1]
while len(kind) < 9:
kind = kind + " "
amount = ledger.money(e[2])
if e[1] == "withdraw":
amount = "-" + amount
elif e[1] == "refused":
amount = "(" + amount + ")"
print(month_label(e[0]) + " " + kind + padded(amount, 13) + padded(ledger.money(e[3]), 14))
def check_books(acct):
b = ledger.books(acct.log)
print("From the log: deposits " + ledger.money(b[0]) + ", withdrawals " + ledger.money(b[1]) + ", interest " + ledger.money(b[2]) + ".")
print("They add up to " + ledger.money(b[3]) + "; the account says " + ledger.money(acct.balance) + ".")
refused = 0
for e in acct.log:
if e[1] == "refused":
refused = refused + 1
if b[3] == acct.balance and b[4] and b[5]:
print("The books agree, every balance on the way was at least 0.00, and " + str(refused) + " refused withdrawal(s) changed nothing.")
else:
print("The books do NOT agree.")
def goal(acct):
g = ask_amount("goal> ")
if g == -1:
print("Cancelled.")
return
if g <= acct.balance:
print("The balance, " + ledger.money(acct.balance) + ", is there already.")
return
m = ask_amount("put in each month> ")
if m == -1:
print("Cancelled.")
return
r = ledger.months_to_goal(acct.balance, acct.rate, m, g)
if r[0] == -1:
print("Putting in " + ledger.money(m) + " a month, it would take more than 100 years.")
return
years = ledger.quotient(r[0], 12)
print("Putting in " + ledger.money(m) + " a month at " + rate_text(acct.rate) + ": " + counted(r[0], "month", "months") + " (" + counted(years, "year", "years") + " " + counted(r[0] % 12, "month", "months") + "), reaching " + ledger.money(r[1]) + " with " + ledger.money(r[2]) + " of it interest.")
def set_rate(acct):
while True:
answer = trim(input("yearly rate in % (0 to 20)> "))
if answer == "":
print("Cancelled.")
return
r = cents_of(answer, 2)
if r >= 0 and r <= 2000:
acct.rate = r
print("The rate is now " + rate_text(r) + " a year, paid monthly.")
return
print("Type a rate like 2.5 or 3.25, from 0 to 20.")
print("== Savings ledger ==")
print("Whole cents, monthly interest, and a balance that never goes below zero.")
acct = ledger.Account(250)
print("The account opens in " + month_label(0) + " at " + rate_text(250) + " a year, paid monthly.")
running = True
while running:
print("")
print("1) deposit 2) withdraw 3) next month 4) statement 5) check the books 6) goal 7) rate 8) quit")
choice = trim(input("choice> "))
if choice == "1":
deposit(acct)
elif choice == "2":
withdraw(acct)
elif choice == "3":
next_month(acct)
elif choice == "4":
statement(acct)
elif choice == "5":
check_books(acct)
elif choice == "6":
goal(acct)
elif choice == "7":
set_rate(acct)
elif choice == "8":
running = False
else:
print("Pick a number from 1 to 8.")
print("Bye.")
ledger.eml
eml# P064 savings ledger - the account, its log and the checks on it. Money is
# whole cents; the yearly rate is in hundredths of a percent (250 = 2.50%).
def quotient(a, b):
# a // b for 0 <= a and b > 0. The numbers here stay far below 2^53, so a
# / b as a float is exact when the answer is whole, and otherwise at least
# 1/b away from a whole number - more than a float's error - so int()
# rounds down correctly.
return int(a / b)
def month_interest(balance, rate):
# One month at the yearly rate: balance x rate / 10000 / 12, to the
# nearest cent, halves up.
return quotient(2 * balance * rate + 120000, 240000)
class Account:
def __init__(self, rate):
0 => self.balance
rate => self.rate
0 => self.month
# each entry: [month, kind, amount, balance after]; kinds deposit,
# withdraw, interest, refused
[] => self.log
def deposit(self, cents):
if cents <= 0:
raise ValueError("a deposit must be more than 0.00")
self.balance + cents => self.balance
self.log + [[self.month, "deposit", cents, self.balance]] => self.log
def withdraw(self, cents):
# The balance is never allowed below zero: a withdrawal that would do
# that is refused by raising, which the caller cannot overlook - the
# corpus case bank-account-invariant's point - and the refusal is
# logged without touching the balance.
if cents <= 0:
raise ValueError("a withdrawal must be more than 0.00")
if cents > self.balance:
self.log + [[self.month, "refused", cents, self.balance]] => self.log
raise ValueError("only " + money(self.balance) + " is in the account")
self.balance - cents => self.balance
self.log + [[self.month, "withdraw", cents, self.balance]] => self.log
def next_month(self):
# Interest for the month that ends, on the balance at its end.
month_interest(self.balance, self.rate) => earned
self.balance + earned => self.balance
self.log + [[self.month, "interest", earned, self.balance]] => self.log
self.month + 1 => self.month
return earned
def money(cents):
# 123456 -> "1,234.56"; negative with a minus in front.
"" => sign
cents => c
if c < 0:
"-" => sign
0 - c => c
str(c % 100) => part
if len(part) == 1:
"0" + part => part
str(quotient(c, 100)) => whole
"" => grouped
while len(whole) > 3:
"," + whole[len(whole) - 3:len(whole)] + grouped => grouped
whole[0:len(whole) - 3] => whole
return sign + whole + grouped + "." + part
def books(log):
# The checks of the corpus case bank-account-invariant, worked out from
# the log alone, not from the account: [deposits, withdrawals, interest,
# the balance they add up to, every running balance >= 0, every entry's
# balance equal to the one before plus its own change].
0 => dep
0 => wd
0 => intr
0 => running
True => never_negative
True => chain
for e in log:
if e[1] == "deposit":
dep + e[2] => dep
running + e[2] => running
elif e[1] == "withdraw":
wd + e[2] => wd
running - e[2] => running
elif e[1] == "interest":
intr + e[2] => intr
running + e[2] => running
if running < 0:
False => never_negative
if e[3] != running:
False => chain
return [dep, wd, intr, running, never_negative, chain]
def months_to_goal(balance, rate, monthly, goal):
# Months until the balance reaches goal, putting in `monthly` at the
# start of each month and earning that month's interest at its end; -1
# when it would take more than 100 years. Returns [months, balance then,
# interest earned on the way].
balance => b
0 => earned
0 => m
while b < goal:
if m == 1200:
return [-1, b, earned]
b + monthly => b
month_interest(b, rate) => i
b + i => b
earned + i => earned
m + 1 => m
return [m, b, earned]
Python projection (ledger.py)
def quotient(a, b):
return int(a / b)
def month_interest(balance, rate):
return quotient(2 * balance * rate + 120000, 240000)
class Account:
def __init__(self, rate):
self.balance = 0
self.rate = rate
self.month = 0
self.log = []
def deposit(self, cents):
if cents <= 0:
raise ValueError("a deposit must be more than 0.00")
self.balance = self.balance + cents
self.log = self.log + [[self.month, "deposit", cents, self.balance]]
def withdraw(self, cents):
if cents <= 0:
raise ValueError("a withdrawal must be more than 0.00")
if cents > self.balance:
self.log = self.log + [[self.month, "refused", cents, self.balance]]
raise ValueError("only " + money(self.balance) + " is in the account")
self.balance = self.balance - cents
self.log = self.log + [[self.month, "withdraw", cents, self.balance]]
def next_month(self):
earned = month_interest(self.balance, self.rate)
self.balance = self.balance + earned
self.log = self.log + [[self.month, "interest", earned, self.balance]]
self.month = self.month + 1
return earned
def money(cents):
sign = ""
c = cents
if c < 0:
sign = "-"
c = 0 - c
part = str(c % 100)
if len(part) == 1:
part = "0" + part
whole = str(quotient(c, 100))
grouped = ""
while len(whole) > 3:
grouped = "," + whole[len(whole) - 3:len(whole)] + grouped
whole = whole[0:len(whole) - 3]
return sign + whole + grouped + "." + part
def books(log):
dep = 0
wd = 0
intr = 0
running = 0
never_negative = True
chain = True
for e in log:
if e[1] == "deposit":
dep = dep + e[2]
running = running + e[2]
elif e[1] == "withdraw":
wd = wd + e[2]
running = running - e[2]
elif e[1] == "interest":
intr = intr + e[2]
running = running + e[2]
if running < 0:
never_negative = False
if e[3] != running:
chain = False
return [dep, wd, intr, running, never_negative, chain]
def months_to_goal(balance, rate, monthly, goal):
b = balance
earned = 0
m = 0
while b < goal:
if m == 1200:
return [-1, b, earned]
b = b + monthly
i = month_interest(b, rate)
b = b + i
earned = earned + i
m = m + 1
return [m, b, earned]