Project P014

Loan calculator

The monthly payment of a loan, worked out exactly in whole numbers and rounded by a stated rule (to the nearest cent or up to the next), the repayment schedule by year and by month, the last payment that clears the balance to exactly 0.00, and the total paid and total interest - from a text menu.

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

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

Works out a loan's monthly payment and its whole repayment schedule: the payment itself, then year by year and month by month how much of each payment is interest and how much pays off the loan, the last payment, the total paid and the total interest. A text menu; the loan lives while the program runs.

  • main.eml - the menu and its questions, with their checks, and what the screen shows
  • loan.eml - the exact payment, the rounding rules, the schedule and the yearly totals
  • text.eml - trimming, whole numbers, decimals read digit by digit, money and rates

Everything is whole numbers. Money is cents and the yearly rate is thousandths of a percent (5.25% is 5250), so the monthly rate is rate / 1,200,000. The standard payment, amount x r x (1 + r)^n / ((1 + r)^n - 1), then becomes amount x rate x A^n / (1,200,000 x (A^n - 1,200,000^n)) with A = 1,200,000 + rate: whole numbers of thousands of digits for a 30-year loan, kept exact, and divided by long division (EML has no //, and / goes through a float that cannot hold them). The screen shows the exact payment to four decimals before any rounding.

The rounding rules, stated:

  • the payment is rounded to the nearest cent (half up), or, if you choose, up to the next cent;
  • each month's interest is the balance times the monthly rate, rounded half up to the cent;
  • the last payment is whatever clears the balance, so the schedule ends at exactly 0.00 and the total of the principal parts is exactly the loan.

So the last payment differs from the others by a few cents, and the two rounding rules show both ways: 300,000 at 6% for 30 years has an exact payment of 1,798.6515...; rounded to the nearest cent it is 1,798.65 and the last payment is 1,800.09, rounded up it is 1,798.66 and the last payment is 1,790.23 - and the cent more each month saves 6.27 of interest.

What is checked: an amount is written like 250000 or 1500.50, more than 0 and at most 10,000,000.00; a yearly rate has at most three decimals and is at most 30% (0 is allowed: then the payment is the amount divided by the months); the term is 1 to 40 whole years; the rounding is 1 or 2; a year to show is one of the loan's. Anything else asks again; nothing cancels.

Sessions: sessions/basic.in takes 300,000 at 6% for 30 years - the first payment is 1,500.00 of interest and 298.65 of principal, and after five years 279,163.14 is still owed - shows it by year, the months of the first and the last year, then the same loan with the payment rounded up and its last year; sessions/bad-input.in asks for the summary and the tables before any loan, cancels, types amounts, rates, terms, roundings and years that are words, out of range or have too many decimals, then takes a loan at 0% whose last payment is 27.70 against 27.78.

Built on the verified corpus cases five-years-of-payments-bought-seven-percent-of-the-house (the same 300,000 loan: each payment first pays the interest on the whole balance) and money-in-cents (amounts kept in whole cents so the parts equal the whole).

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

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 6
Pick a number from 1 to 5.

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 2
No loan yet.

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 3
No loan yet.

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 4
No loan yet.

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 1
amount> 
Cancelled.

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 1
amount> abc
Type an amount like 250000 or 1500.50, or nothing to cancel.
amount> 0
An amount is more than 0.
amount> 20000000
An amount is at most 10,000,000.00.
amount> 1000
yearly rate in %> seven
Type a yearly rate like 5.25 (at most three decimals), or nothing to cancel.
yearly rate in %> 5.1234
Type a yearly rate like 5.25 (at most three decimals), or nothing to cancel.
yearly rate in %> 31
The yearly rate is at most 30%.
yearly rate in %> 0
years> 0
Type a whole number of years from 1 to 40, or nothing to cancel.
years> 41
Type a whole number of years from 1 to 40, or nothing to cancel.
years> 3
payment rounding (1 = nearest cent, 2 = up to the next cent)> 3
Type 1 or 2, or nothing to cancel.
payment rounding (1 = nearest cent, 2 = up to the next cent)> 1

-- loan --
  amount                    1,000.00
  yearly rate                 0.000%
  term                       3 years
  payments                        36
  exact payment           27.7777...
  monthly payment              27.78   (to the nearest cent)
  last payment                 27.70   (payment 36 clears the balance)
  total paid                1,000.00
  total interest                0.00

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 4
year (1 to 3)> 0
Type a year from 1 to 3, or nothing to cancel.
year (1 to 3)> 4
Type a year from 1 to 3, or nothing to cancel.
year (1 to 3)> 3

-- year 3: payments 25 to 36 --
  payment        paid    interest    principal         balance
       25       27.78        0.00        27.78          305.50
       26       27.78        0.00        27.78          277.72
       27       27.78        0.00        27.78          249.94
       28       27.78        0.00        27.78          222.16
       29       27.78        0.00        27.78          194.38
       30       27.78        0.00        27.78          166.60
       31       27.78        0.00        27.78          138.82
       32       27.78        0.00        27.78          111.04
       33       27.78        0.00        27.78           83.26
       34       27.78        0.00        27.78           55.48
       35       27.78        0.00        27.78           27.70
       36       27.70        0.00        27.70            0.00

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 2

-- loan --
  amount                    1,000.00
  yearly rate                 0.000%
  term                       3 years
  payments                        36
  exact payment           27.7777...
  monthly payment              27.78   (to the nearest cent)
  last payment                 27.70   (payment 36 clears the balance)
  total paid                1,000.00
  total interest                0.00

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 5
Bye.
What was typed (26 lines)
6
2
3
4
1

1
abc
0
20000000
1000
seven
5.1234
31
0
0
41
3
3
1
4
0
4
3
2
5

basic

interpreter: byte-equal

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 1
amount> 300000
yearly rate in %> 6
years> 30
payment rounding (1 = nearest cent, 2 = up to the next cent)> 1

-- loan --
  amount                  300,000.00
  yearly rate                 6.000%
  term                      30 years
  payments                       360
  exact payment        1,798.6515...
  monthly payment           1,798.65   (to the nearest cent)
  last payment              1,800.09   (payment 360 clears the balance)
  total paid              647,515.44
  total interest          347,515.44

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 3

-- by year --
  year      interest     principal         balance
     1     17,899.80      3,684.00      296,316.00
     2     17,672.54      3,911.26      292,404.74
     3     17,431.32      4,152.48      288,252.26
     4     17,175.19      4,408.61      283,843.65
     5     16,903.29      4,680.51      279,163.14
     6     16,614.61      4,969.19      274,193.95
     7     16,308.11      5,275.69      268,918.26
     8     15,982.74      5,601.06      263,317.20
     9     15,637.26      5,946.54      257,370.66
    10     15,270.50      6,313.30      251,057.36
    11     14,881.12      6,702.68      244,354.68
    12     14,467.71      7,116.09      237,238.59
    13     14,028.78      7,555.02      229,683.57
    14     13,562.86      8,020.94      221,662.63
    15     13,068.10      8,515.70      213,146.93
    16     12,542.88      9,040.92      204,106.01
    17     11,985.26      9,598.54      194,507.47
    18     11,393.25     10,190.55      184,316.92
    19     10,764.69     10,819.11      173,497.81
    20     10,097.41     11,486.39      162,011.42
    21      9,388.94     12,194.86      149,816.56
    22      8,636.79     12,947.01      136,869.55
    23      7,838.27     13,745.53      123,124.02
    24      6,990.46     14,593.34      108,530.68
    25      6,090.37     15,493.43       93,037.25
    26      5,134.77     16,449.03       76,588.22
    27      4,120.23     17,463.57       59,124.65
    28      3,043.13     18,540.67       40,583.98
    29      1,899.57     19,684.23       20,899.75
    30        685.49     20,899.75            0.00
   all    347,515.44    300,000.00

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 4
year (1 to 30)> 1

-- year 1: payments 1 to 12 --
  payment        paid    interest    principal         balance
        1    1,798.65    1,500.00       298.65      299,701.35
        2    1,798.65    1,498.51       300.14      299,401.21
        3    1,798.65    1,497.01       301.64      299,099.57
        4    1,798.65    1,495.50       303.15      298,796.42
        5    1,798.65    1,493.98       304.67      298,491.75
        6    1,798.65    1,492.46       306.19      298,185.56
        7    1,798.65    1,490.93       307.72      297,877.84
        8    1,798.65    1,489.39       309.26      297,568.58
        9    1,798.65    1,487.84       310.81      297,257.77
       10    1,798.65    1,486.29       312.36      296,945.41
       11    1,798.65    1,484.73       313.92      296,631.49
       12    1,798.65    1,483.16       315.49      296,316.00

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 4
year (1 to 30)> 30

-- year 30: payments 349 to 360 --
  payment        paid    interest    principal         balance
      349    1,798.65      104.50     1,694.15       19,205.60
      350    1,798.65       96.03     1,702.62       17,502.98
      351    1,798.65       87.51     1,711.14       15,791.84
      352    1,798.65       78.96     1,719.69       14,072.15
      353    1,798.65       70.36     1,728.29       12,343.86
      354    1,798.65       61.72     1,736.93       10,606.93
      355    1,798.65       53.03     1,745.62        8,861.31
      356    1,798.65       44.31     1,754.34        7,106.97
      357    1,798.65       35.53     1,763.12        5,343.85
      358    1,798.65       26.72     1,771.93        3,571.92
      359    1,798.65       17.86     1,780.79        1,791.13
      360    1,800.09        8.96     1,791.13            0.00

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 1
amount> 300000
yearly rate in %> 6
years> 30
payment rounding (1 = nearest cent, 2 = up to the next cent)> 2

-- loan --
  amount                  300,000.00
  yearly rate                 6.000%
  term                      30 years
  payments                       360
  exact payment        1,798.6515...
  monthly payment           1,798.66   (up to the next cent)
  last payment              1,790.23   (payment 360 clears the balance)
  total paid              647,509.17
  total interest          347,509.17

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 4
year (1 to 30)> 30

-- year 30: payments 349 to 360 --
  payment        paid    interest    principal         balance
      349    1,798.66      104.45     1,694.21       19,196.36
      350    1,798.66       95.98     1,702.68       17,493.68
      351    1,798.66       87.47     1,711.19       15,782.49
      352    1,798.66       78.91     1,719.75       14,062.74
      353    1,798.66       70.31     1,728.35       12,334.39
      354    1,798.66       61.67     1,736.99       10,597.40
      355    1,798.66       52.99     1,745.67        8,851.73
      356    1,798.66       44.26     1,754.40        7,097.33
      357    1,798.66       35.49     1,763.17        5,334.16
      358    1,798.66       26.67     1,771.99        3,562.17
      359    1,798.66       17.81     1,780.85        1,781.32
      360    1,790.23        8.91     1,781.32            0.00

== Loan calculator ==
1) new loan  2) summary  3) by year  4) months of a year  5) quit
choice> 5
Bye.
What was typed (18 lines)
1
300000
6
30
1
3
4
1
4
30
1
300000
6
30
2
4
30
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
# P014 loan calculator: the monthly payment of a loan, its repayment
# schedule, the total paid and the total interest - worked out exactly, with
# the rounding rules stated: the payment is rounded to the nearest cent or up
# to the next cent, every month's interest is rounded half up to the cent, and
# the last payment clears the balance to exactly 0.00.
import loan
import text

def row(label, value):
    return "  " + ("%-18s" % label) + ("%16s" % value)

def ask_number(prompt, low, high, message):
    # A whole number from low to high, asked again until one is typed; -1 if
    # the answer is empty, which cancels.
    while True:
        text.trim(input(prompt)) => answer
        if answer == "":
            return 0 - 1
        text.number(answer) => n
        if n >= low and n <= high:
            return n
        message ^0

def ask_amount():
    # An amount in cents, asked again until a good one is typed; -1 cancels.
    while True:
        text.trim(input("amount> ")) => answer
        if answer == "":
            return 0 - 1
        text.decimal(answer, 2) => c
        if c == 0 - 1:
            "Type an amount like 250000 or 1500.50, or nothing to cancel." ^0
        elif c == 0:
            "An amount is more than 0." ^0
        elif c > 1000000000:
            "An amount is at most 10,000,000.00." ^0
        else:
            return c

def ask_rate():
    # A yearly rate in thousandths of a percent; -1 cancels.
    while True:
        text.trim(input("yearly rate in %> ")) => answer
        if answer == "":
            return 0 - 1
        text.decimal(answer, 3) => r
        if r == 0 - 1:
            "Type a yearly rate like 5.25 (at most three decimals), or nothing to cancel." ^0
        elif r > 30000:
            "The yearly rate is at most 30%." ^0
        else:
            return r

def show_summary(l):
    # l is [amount, rate, years, rule, payment, rows, exact payment].
    l[5] => rows
    0 => total_paid
    0 => total_interest
    for r in rows:
        total_paid + r[1] => total_paid
        total_interest + r[2] => total_interest
    l[6] => f
    "" => dots
    if (f[0] * 100) % f[1] != 0:
        "..." => dots
    "to the nearest cent" => how
    if l[3] == "up":
        "up to the next cent" => how
    str(l[2]) + " years" => term
    if l[2] == 1:
        "1 year" => term
    rows[len(rows) - 1] => last
    "" ^0
    "-- loan --" ^0
    row("amount", text.money(l[0])) ^0
    row("yearly rate", text.rate_text(l[1])) ^0
    row("term", term) ^0
    row("payments", str(len(rows))) ^0
    row("exact payment", text.fixed(loan.quotient(f[0] * 100, f[1]), 4) + dots) ^0
    (row("monthly payment", text.money(l[4])) + "   (" + how + ")") ^0
    (row("last payment", text.money(last[1])) + "   (payment " + str(last[0]) + " clears the balance)") ^0
    row("total paid", text.money(total_paid)) ^0
    row("total interest", text.money(total_interest)) ^0

None => current
True => running
while running:
    "" ^0
    "== Loan calculator ==" ^0
    "1) new loan  2) summary  3) by year  4) months of a year  5) quit" ^0
    text.trim(input("choice> ")) => choice
    if choice == "1":
        ask_amount() => amount
        0 - 1 => rate
        0 - 1 => years
        0 - 1 => rule
        if amount != 0 - 1:
            ask_rate() => rate
        if rate != 0 - 1:
            ask_number("years> ", 1, 40, "Type a whole number of years from 1 to 40, or nothing to cancel.") => years
        if years != 0 - 1:
            ask_number("payment rounding (1 = nearest cent, 2 = up to the next cent)> ", 1, 2, "Type 1 or 2, or nothing to cancel.") => rule
        if rule == 0 - 1:
            "Cancelled." ^0
        else:
            "nearest" => how
            if rule == 2:
                "up" => how
            loan.exact_payment(amount, rate, 12 * years) => f
            loan.rounded(f, how) => payment
            loan.schedule(amount, rate, 12 * years, payment) => rows
            [amount, rate, years, how, payment, rows, f] => current
            show_summary(current)
    elif choice == "2":
        if current == None:
            "No loan yet." ^0
        else:
            show_summary(current)
    elif choice == "3":
        if current == None:
            "No loan yet." ^0
        else:
            "" ^0
            "-- by year --" ^0
            ("  " + ("%4s" % "year") + ("%14s" % "interest") + ("%14s" % "principal") + ("%16s" % "balance")) ^0
            0 => all_interest
            0 => all_principal
            for y in loan.by_year(current[5]):
                ("  " + ("%4d" % y[0]) + ("%14s" % text.money(y[1])) + ("%14s" % text.money(y[2])) + ("%16s" % text.money(y[3]))) ^0
                all_interest + y[1] => all_interest
                all_principal + y[2] => all_principal
            ("  " + ("%4s" % "all") + ("%14s" % text.money(all_interest)) + ("%14s" % text.money(all_principal))) ^0
    elif choice == "4":
        if current == None:
            "No loan yet." ^0
        else:
            current[2] => last_year
            ask_number("year (1 to " + str(last_year) + ")> ", 1, last_year, "Type a year from 1 to " + str(last_year) + ", or nothing to cancel.") => y
            if y == 0 - 1:
                "Cancelled." ^0
            else:
                "" ^0
                ("-- year " + str(y) + ": payments " + str(12 * y - 11) + " to " + str(12 * y) + " --") ^0
                ("  " + ("%7s" % "payment") + ("%12s" % "paid") + ("%12s" % "interest") + ("%13s" % "principal") + ("%16s" % "balance")) ^0
                for r in current[5]:
                    if r[0] > 12 * y - 12 and r[0] <= 12 * y:
                        ("  " + ("%7d" % r[0]) + ("%12s" % text.money(r[1])) + ("%12s" % text.money(r[2])) + ("%13s" % text.money(r[3])) + ("%16s" % text.money(r[4]))) ^0
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import loan
import text

def row(label, value):
    return "  " + "%-18s" % label + "%16s" % value

def ask_number(prompt, low, high, message):
    while True:
        answer = text.trim(input(prompt))
        if answer == "":
            return 0 - 1
        n = text.number(answer)
        if n >= low and n <= high:
            return n
        print(message)

def ask_amount():
    while True:
        answer = text.trim(input("amount> "))
        if answer == "":
            return 0 - 1
        c = text.decimal(answer, 2)
        if c == 0 - 1:
            print("Type an amount like 250000 or 1500.50, or nothing to cancel.")
        elif c == 0:
            print("An amount is more than 0.")
        elif c > 1000000000:
            print("An amount is at most 10,000,000.00.")
        else:
            return c

def ask_rate():
    while True:
        answer = text.trim(input("yearly rate in %> "))
        if answer == "":
            return 0 - 1
        r = text.decimal(answer, 3)
        if r == 0 - 1:
            print("Type a yearly rate like 5.25 (at most three decimals), or nothing to cancel.")
        elif r > 30000:
            print("The yearly rate is at most 30%.")
        else:
            return r

def show_summary(l):
    rows = l[5]
    total_paid = 0
    total_interest = 0
    for r in rows:
        total_paid = total_paid + r[1]
        total_interest = total_interest + r[2]
    f = l[6]
    dots = ""
    if f[0] * 100 % f[1] != 0:
        dots = "..."
    how = "to the nearest cent"
    if l[3] == "up":
        how = "up to the next cent"
    term = str(l[2]) + " years"
    if l[2] == 1:
        term = "1 year"
    last = rows[len(rows) - 1]
    print("")
    print("-- loan --")
    print(row("amount", text.money(l[0])))
    print(row("yearly rate", text.rate_text(l[1])))
    print(row("term", term))
    print(row("payments", str(len(rows))))
    print(row("exact payment", text.fixed(loan.quotient(f[0] * 100, f[1]), 4) + dots))
    print(row("monthly payment", text.money(l[4])) + "   (" + how + ")")
    print(row("last payment", text.money(last[1])) + "   (payment " + str(last[0]) + " clears the balance)")
    print(row("total paid", text.money(total_paid)))
    print(row("total interest", text.money(total_interest)))

current = None
running = True
while running:
    print("")
    print("== Loan calculator ==")
    print("1) new loan  2) summary  3) by year  4) months of a year  5) quit")
    choice = text.trim(input("choice> "))
    if choice == "1":
        amount = ask_amount()
        rate = 0 - 1
        years = 0 - 1
        rule = 0 - 1
        if amount != 0 - 1:
            rate = ask_rate()
        if rate != 0 - 1:
            years = ask_number("years> ", 1, 40, "Type a whole number of years from 1 to 40, or nothing to cancel.")
        if years != 0 - 1:
            rule = ask_number("payment rounding (1 = nearest cent, 2 = up to the next cent)> ", 1, 2, "Type 1 or 2, or nothing to cancel.")
        if rule == 0 - 1:
            print("Cancelled.")
        else:
            how = "nearest"
            if rule == 2:
                how = "up"
            f = loan.exact_payment(amount, rate, 12 * years)
            payment = loan.rounded(f, how)
            rows = loan.schedule(amount, rate, 12 * years, payment)
            current = [amount, rate, years, how, payment, rows, f]
            show_summary(current)
    elif choice == "2":
        if current == None:
            print("No loan yet.")
        else:
            show_summary(current)
    elif choice == "3":
        if current == None:
            print("No loan yet.")
        else:
            print("")
            print("-- by year --")
            print("  " + "%4s" % "year" + "%14s" % "interest" + "%14s" % "principal" + "%16s" % "balance")
            all_interest = 0
            all_principal = 0
            for y in loan.by_year(current[5]):
                print("  " + "%4d" % y[0] + "%14s" % text.money(y[1]) + "%14s" % text.money(y[2]) + "%16s" % text.money(y[3]))
                all_interest = all_interest + y[1]
                all_principal = all_principal + y[2]
            print("  " + "%4s" % "all" + "%14s" % text.money(all_interest) + "%14s" % text.money(all_principal))
    elif choice == "4":
        if current == None:
            print("No loan yet.")
        else:
            last_year = current[2]
            y = ask_number("year (1 to " + str(last_year) + ")> ", 1, last_year, "Type a year from 1 to " + str(last_year) + ", or nothing to cancel.")
            if y == 0 - 1:
                print("Cancelled.")
            else:
                print("")
                print("-- year " + str(y) + ": payments " + str(12 * y - 11) + " to " + str(12 * y) + " --")
                print("  " + "%7s" % "payment" + "%12s" % "paid" + "%12s" % "interest" + "%13s" % "principal" + "%16s" % "balance")
                for r in current[5]:
                    if r[0] > 12 * y - 12 and r[0] <= 12 * y:
                        print("  " + "%7d" % r[0] + "%12s" % text.money(r[1]) + "%12s" % text.money(r[2]) + "%13s" % text.money(r[3]) + "%16s" % text.money(r[4]))
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")

loan.eml

eml
# P014 loan calculator - the monthly payment and the schedule, worked out
# exactly. Money is whole cents, and the yearly rate is kept in thousandths of
# a percent (5.25% is 5250), so the monthly rate is R / 1,200,000 and no float
# is involved anywhere.

1200000 => B

def quotient(a, b):
    # a // b for whole numbers a >= 0 and b > 0, exact at any size. Long
    # division by doubling: take away the largest b * 2^k that still fits.
    0 => q
    while a >= b:
        b => m
        1 => k
        while m + m <= a:
            m + m => m
            k + k => k
        a - m => a
        q + k => q
    return q

def exact_payment(amount, rate, months):
    # The exact monthly payment in cents, as a fraction [top, bottom]. With
    # r = rate / B it is amount * r * (1 + r)^months / ((1 + r)^months - 1),
    # which is amount * rate * A^months / (B * (A^months - B^months)) for
    # A = B + rate - whole numbers with thousands of digits, kept exact.
    # With no interest it is amount / months.
    if rate == 0:
        return [amount, months]
    B + rate => A
    1 => a_pow
    1 => b_pow
    for i in [1:months]:
        a_pow * A => a_pow
        b_pow * B => b_pow
    return [amount * rate * a_pow, B * (a_pow - b_pow)]

def rounded(f, rule):
    # The fraction f of cents as whole cents: rule "nearest" rounds half up,
    # rule "up" rounds any part of a cent up.
    if rule == "up":
        return quotient(f[0] + f[1] - 1, f[1])
    return quotient(2 * f[0] + f[1], 2 * f[1])

def half_up(n, d):
    # n / d rounded half up, for whole numbers n >= 0 and d > 0 that stay
    # below 2^53, where the float division of an exact multiple is exact.
    2 * n + d => t
    return int((t - t % (2 * d)) / (2 * d))

def schedule(amount, rate, months, payment):
    # One row [number, paid, interest, principal, balance after] per month.
    # Each month's interest is the balance times the monthly rate, rounded
    # half up to the cent; the last payment is whatever clears the balance.
    [] => rows
    amount => balance
    for k in [1:months]:
        half_up(balance * rate, B) => interest
        payment => paid
        if k == months or paid > balance + interest:
            balance + interest => paid
        paid - interest => principal
        balance - principal => balance
        rows + [[k, paid, interest, principal, balance]] => rows
        if balance == 0:
            return rows
    return rows

def by_year(rows):
    # One row [year, interest, principal, balance at the end] per year.
    [] => years
    for r in rows:
        int((r[0] - 1 - (r[0] - 1) % 12) / 12) + 1 => y
        if len(years) < y:
            years + [[y, 0, 0, 0]] => years
        years[y - 1][1] + r[2] => years[y - 1][1]
        years[y - 1][2] + r[3] => years[y - 1][2]
        r[4] => years[y - 1][3]
    return years
Python projection (loan.py)
B = 1200000

def quotient(a, b):
    q = 0
    while a >= b:
        m = b
        k = 1
        while m + m <= a:
            m = m + m
            k = k + k
        a = a - m
        q = q + k
    return q

def exact_payment(amount, rate, months):
    if rate == 0:
        return [amount, months]
    A = B + rate
    a_pow = 1
    b_pow = 1
    for i in range(1, months+1):
        a_pow = a_pow * A
        b_pow = b_pow * B
    return [amount * rate * a_pow, B * (a_pow - b_pow)]

def rounded(f, rule):
    if rule == "up":
        return quotient(f[0] + f[1] - 1, f[1])
    return quotient(2 * f[0] + f[1], 2 * f[1])

def half_up(n, d):
    t = 2 * n + d
    return int((t - t % (2 * d)) / (2 * d))

def schedule(amount, rate, months, payment):
    rows = []
    balance = amount
    for k in range(1, months+1):
        interest = half_up(balance * rate, B)
        paid = payment
        if k == months or paid > balance + interest:
            paid = balance + interest
        principal = paid - interest
        balance = balance - principal
        rows = rows + [[k, paid, interest, principal, balance]]
        if balance == 0:
            return rows
    return rows

def by_year(rows):
    years = []
    for r in rows:
        y = int((r[0] - 1 - (r[0] - 1) % 12) / 12) + 1
        if len(years) < y:
            years = years + [[y, 0, 0, 0]]
        years[y - 1][1] = years[y - 1][1] + r[2]
        years[y - 1][2] = years[y - 1][2] + r[3]
        years[y - 1][3] = r[4]
    return years

text.eml

eml
# P014 loan calculator - reading what is typed and writing money and rates.
# Amounts are whole cents and rates thousandths of a percent from the moment
# they are typed: "5.25" is read digit by digit into 5250 and never passes
# through a float. The interpreter that checks every session does not run
# string methods yet, so the text handling is written out here.

def trim(s):
    # s without the spaces at either end.
    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):
    # The value of s if it is digits only (at least one), otherwise -1.
    if s == "":
        return 0 - 1
    0 => n
    for c in s:
        if not (c in "0123456789"):
            return 0 - 1
        n * 10 + int(c) => n
    return n

def decimal(s, places):
    # The number in s times 10^places, or -1 if s is not digits followed by a
    # point and 1 to places digits, if any.
    0 => whole
    0 => whole_digits
    0 => frac
    0 => frac_digits
    False => point
    for c in s:
        if c == ".":
            if point:
                return 0 - 1
            True => point
        elif c in "0123456789":
            if point:
                frac * 10 + int(c) => frac
                frac_digits + 1 => frac_digits
            else:
                whole * 10 + int(c) => whole
                whole_digits + 1 => whole_digits
        else:
            return 0 - 1
    if whole_digits == 0:
        return 0 - 1
    if point and (frac_digits == 0 or frac_digits > places):
        return 0 - 1
    while frac_digits < places:
        frac * 10 => frac
        frac_digits + 1 => frac_digits
    while places > 0:
        whole * 10 => whole
        places - 1 => places
    return whole + frac

def grouped(digits):
    # "1234567" as "1,234,567".
    "" => out
    len(digits) => i
    while i > 3:
        "," + digits[i - 3:i] + out => out
        i - 3 => i
    return digits[0:i] + out

def fixed(n, places):
    # The whole number n >= 0 read as having places decimals: 179865 with 2
    # places is 1,798.65.
    str(n) => s
    while len(s) < places + 1:
        "0" + s => s
    return grouped(s[0:len(s) - places]) + "." + s[len(s) - places:len(s)]

def money(c):
    return fixed(c, 2)

def rate_text(r):
    # A rate in thousandths of a percent as 5.250%.
    return fixed(r, 3) + "%"
Python projection (text.py)
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 number(s):
    if s == "":
        return 0 - 1
    n = 0
    for c in s:
        if not c in "0123456789":
            return 0 - 1
        n = n * 10 + int(c)
    return n

def decimal(s, places):
    whole = 0
    whole_digits = 0
    frac = 0
    frac_digits = 0
    point = False
    for c in s:
        if c == ".":
            if point:
                return 0 - 1
            point = True
        elif c in "0123456789":
            if point:
                frac = frac * 10 + int(c)
                frac_digits = frac_digits + 1
            else:
                whole = whole * 10 + int(c)
                whole_digits = whole_digits + 1
        else:
            return 0 - 1
    if whole_digits == 0:
        return 0 - 1
    if point and (frac_digits == 0 or frac_digits > places):
        return 0 - 1
    while frac_digits < places:
        frac = frac * 10
        frac_digits = frac_digits + 1
    while places > 0:
        whole = whole * 10
        places = places - 1
    return whole + frac

def grouped(digits):
    out = ""
    i = len(digits)
    while i > 3:
        out = "," + digits[i - 3:i] + out
        i = i - 3
    return digits[0:i] + out

def fixed(n, places):
    s = str(n)
    while len(s) < places + 1:
        s = "0" + s
    return grouped(s[0:len(s) - places]) + "." + s[len(s) - places:len(s)]

def money(c):
    return fixed(c, 2)

def rate_text(r):
    return fixed(r, 3) + "%"

Built on these corpus cases