{
  "id": "P064",
  "slug": "savings-ledger",
  "key": "P064-savings-ledger",
  "title": "Savings ledger",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P064 - Savings ledger\n\nA savings account kept in whole cents. You can deposit, and withdraw - a\nwithdrawal that would take the balance below zero is refused. Each month\nends with compound interest, rounded to the cent. The statement shows\nevery entry with its running balance. A check of the books works the\nbalance out again from the log alone, and a goal says how many months a\nsavings target would take.\n\n- `main.eml` - the menu, reading amounts and the rate, and the statement,\n  books and goal messages\n- `ledger.eml` - the account, interest, money formatting, the books check\n  and the goal\n\nHow each part works:\n\n- Money is whole cents, and the yearly rate is in hundredths of a percent:\n  2.50% is 250. A month's interest is balance x rate / 10000 / 12, rounded\n  to the nearest cent, halves up, and is added to the balance, so the next\n  month earns interest on it too.\n- The account is a class. A deposit or withdrawal that breaks the rules\n  raises an error instead of returning a status that could be ignored, as\n  in the corpus cases `bank-account-simulator` and\n  `bank-account-invariant`. The menu catches it and says why. A refused\n  withdrawal is written in the log but changes nothing.\n- Checking the books follows the second corpus case. It adds the deposits,\n  withdrawals and interest from the log, without asking the account, and\n  checks three things: the total equals the balance, every running balance\n  on the way was at least 0.00, and every entry's balance equals the one\n  before it plus its own change.\n- A goal puts the monthly amount in at the start of each month and earns\n  that month's interest at its end, until the target is reached. It gives\n  up after 100 years.\n\nWhat is checked: menu choices 1 to 8; amounts as whole numbers or with up\nto two decimals, commas between thousands allowed, up to 1,000,000.00;\ndeposits and withdrawals above 0.00; rates from 0 to 20%. An empty answer\ncancels.\n\nSessions: `sessions/basic.in` gives:\n- deposits of 1,000 and 250.50;\n- a month at 2.50%, which earns 2.61 (1,250.50 x 2.5% / 12 = 2.6052);\n- a withdrawal of 300, then two more months;\n- the statement, and the books agreeing at 957.09;\n- the months to 10,000 at 500 a month: 18 at 2.50%, and again 18 at\n  4.00%, with 349.56 of interest instead of 216.79.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- a withdrawal from the empty account;\n- amounts abc, -5, 12.345 and 1000000.01, and a deposit of 0;\n- 1,500 with a comma, then a withdrawal of 2,000, which is refused;\n- rates 25 and x;\n- a goal below the balance;\n- a cancelled goal;\n- at 0%, 1,000,000 at 1.00 a month, which is more than 100 years;\n- the books with two refused withdrawals, and the statement.\n\nBuilt on the verified corpus cases `bank-account-simulator` (an account\nclass that raises on a bad withdrawal) and `bank-account-invariant` (the\nbalance never negative, checked again from the log of allowed\noperations).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P064 savings ledger: a savings account in whole cents - deposits,\n# withdrawals that can never take it below zero, monthly compound interest\n# - with a statement, a check of the books from the log alone, and how long\n# a goal would take.\nimport ledger\n\ndef trim(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef cents_of(s, places):\n    # \"120\", \"120.5\", \"1,200.50\" as a whole number in 1/10^places, or -1.\n    # Commas between the thousands are skipped.\n    \"\" => t\n    for ch in s:\n        if ch != \",\":\n            t + ch => t\n    0 => k\n    while k < len(t) and t[k] != \".\":\n        k + 1 => k\n    t[0:k] => whole\n    \"\" => part\n    if k < len(t):\n        t[k + 1:len(t)] => part\n    if whole == \"\" or len(whole) > 7 or len(part) > places:\n        return -1\n    for ch in whole + part:\n        if not (ch >= \"0\" and ch <= \"9\"):\n            return -1\n    while len(part) < places:\n        part + \"0\" => part\n    0 => n\n    for ch in whole + part:\n        n * 10 + int(ch) => n\n    return n\n\ndef month_label(m):\n    # Month 0 is 2026-10.\n    9 + m => k\n    2026 + ledger.quotient(k, 12) => y\n    str(k % 12 + 1) => mm\n    if len(mm) == 1:\n        \"0\" + mm => mm\n    return str(y) + \"-\" + mm\n\ndef rate_text(rate):\n    str(rate % 100) => part\n    if len(part) == 1:\n        \"0\" + part => part\n    return str(ledger.quotient(rate, 100)) + \".\" + part + \"%\"\n\ndef ask_amount(prompt):\n    while True:\n        trim(input(prompt)) => answer\n        if answer == \"\":\n            return -1\n        cents_of(answer, 2) => c\n        if c >= 0 and c <= 100000000:\n            return c\n        \"Type an amount like 250 or 12.50, up to 1,000,000.00.\" ^0\n\ndef deposit(acct):\n    ask_amount(\"deposit> \") => c\n    if c == -1:\n        \"Cancelled.\" ^0\n        return\n    try:\n        acct.deposit(c)\n        (\"Deposited \" + ledger.money(c) + \" in \" + month_label(acct.month) + \". Balance \" + ledger.money(acct.balance) + \".\") ^0\n    except ValueError as e:\n        (\"Not done: \" + str(e) + \".\") ^0\n\ndef withdraw(acct):\n    ask_amount(\"withdraw> \") => c\n    if c == -1:\n        \"Cancelled.\" ^0\n        return\n    try:\n        acct.withdraw(c)\n        (\"Withdrew \" + ledger.money(c) + \". Balance \" + ledger.money(acct.balance) + \".\") ^0\n    except ValueError as e:\n        (\"Refused: \" + str(e) + \"; the balance stays \" + ledger.money(acct.balance) + \".\") ^0\n\ndef next_month(acct):\n    month_label(acct.month) => ending\n    acct.next_month() => earned\n    (ending + \" closes: interest \" + ledger.money(earned) + \" at \" + rate_text(acct.rate) + \" a year. Balance \" + ledger.money(acct.balance) + \"; now \" + month_label(acct.month) + \".\") ^0\n\ndef counted(n, one, many):\n    if n == 1:\n        return \"1 \" + one\n    return str(n) + \" \" + many\n\ndef padded(s, width):\n    while len(s) < width:\n        \" \" + s => s\n    return s\n\ndef statement(acct):\n    if len(acct.log) == 0:\n        \"Nothing has happened in the account yet.\" ^0\n        return\n    \"month     entry           amount       balance\" ^0\n    for e in acct.log:\n        e[1] => kind\n        while len(kind) < 9:\n            kind + \" \" => kind\n        ledger.money(e[2]) => amount\n        if e[1] == \"withdraw\":\n            \"-\" + amount => amount\n        elif e[1] == \"refused\":\n            \"(\" + amount + \")\" => amount\n        (month_label(e[0]) + \"   \" + kind + padded(amount, 13) + padded(ledger.money(e[3]), 14)) ^0\n\ndef check_books(acct):\n    ledger.books(acct.log) => b\n    (\"From the log: deposits \" + ledger.money(b[0]) + \", withdrawals \" + ledger.money(b[1]) + \", interest \" + ledger.money(b[2]) + \".\") ^0\n    (\"They add up to \" + ledger.money(b[3]) + \"; the account says \" + ledger.money(acct.balance) + \".\") ^0\n    0 => refused\n    for e in acct.log:\n        if e[1] == \"refused\":\n            refused + 1 => refused\n    if b[3] == acct.balance and b[4] and b[5]:\n        (\"The books agree, every balance on the way was at least 0.00, and \" + str(refused) + \" refused withdrawal(s) changed nothing.\") ^0\n    else:\n        \"The books do NOT agree.\" ^0\n\ndef goal(acct):\n    ask_amount(\"goal> \") => g\n    if g == -1:\n        \"Cancelled.\" ^0\n        return\n    if g <= acct.balance:\n        (\"The balance, \" + ledger.money(acct.balance) + \", is there already.\") ^0\n        return\n    ask_amount(\"put in each month> \") => m\n    if m == -1:\n        \"Cancelled.\" ^0\n        return\n    ledger.months_to_goal(acct.balance, acct.rate, m, g) => r\n    if r[0] == -1:\n        (\"Putting in \" + ledger.money(m) + \" a month, it would take more than 100 years.\") ^0\n        return\n    ledger.quotient(r[0], 12) => years\n    (\"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\n\ndef set_rate(acct):\n    while True:\n        trim(input(\"yearly rate in % (0 to 20)> \")) => answer\n        if answer == \"\":\n            \"Cancelled.\" ^0\n            return\n        cents_of(answer, 2) => r\n        if r >= 0 and r <= 2000:\n            r => acct.rate\n            (\"The rate is now \" + rate_text(r) + \" a year, paid monthly.\") ^0\n            return\n        \"Type a rate like 2.5 or 3.25, from 0 to 20.\" ^0\n\n\"== Savings ledger ==\" ^0\n\"Whole cents, monthly interest, and a balance that never goes below zero.\" ^0\nledger.Account(250) => acct\n(\"The account opens in \" + month_label(0) + \" at \" + rate_text(250) + \" a year, paid monthly.\") ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\" ^0\n    trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        deposit(acct)\n    elif choice == \"2\":\n        withdraw(acct)\n    elif choice == \"3\":\n        next_month(acct)\n    elif choice == \"4\":\n        statement(acct)\n    elif choice == \"5\":\n        check_books(acct)\n    elif choice == \"6\":\n        goal(acct)\n    elif choice == \"7\":\n        set_rate(acct)\n    elif choice == \"8\":\n        False => running\n    else:\n        \"Pick a number from 1 to 8.\" ^0\n\"Bye.\" ^0\n",
      "python": "import ledger\n\ndef trim(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef cents_of(s, places):\n    t = \"\"\n    for ch in s:\n        if ch != \",\":\n            t = t + ch\n    k = 0\n    while k < len(t) and t[k] != \".\":\n        k = k + 1\n    whole = t[0:k]\n    part = \"\"\n    if k < len(t):\n        part = t[k + 1:len(t)]\n    if whole == \"\" or len(whole) > 7 or len(part) > places:\n        return -1\n    for ch in whole + part:\n        if not (ch >= \"0\" and ch <= \"9\"):\n            return -1\n    while len(part) < places:\n        part = part + \"0\"\n    n = 0\n    for ch in whole + part:\n        n = n * 10 + int(ch)\n    return n\n\ndef month_label(m):\n    k = 9 + m\n    y = 2026 + ledger.quotient(k, 12)\n    mm = str(k % 12 + 1)\n    if len(mm) == 1:\n        mm = \"0\" + mm\n    return str(y) + \"-\" + mm\n\ndef rate_text(rate):\n    part = str(rate % 100)\n    if len(part) == 1:\n        part = \"0\" + part\n    return str(ledger.quotient(rate, 100)) + \".\" + part + \"%\"\n\ndef ask_amount(prompt):\n    while True:\n        answer = trim(input(prompt))\n        if answer == \"\":\n            return -1\n        c = cents_of(answer, 2)\n        if c >= 0 and c <= 100000000:\n            return c\n        print(\"Type an amount like 250 or 12.50, up to 1,000,000.00.\")\n\ndef deposit(acct):\n    c = ask_amount(\"deposit> \")\n    if c == -1:\n        print(\"Cancelled.\")\n        return\n    try:\n        acct.deposit(c)\n        print(\"Deposited \" + ledger.money(c) + \" in \" + month_label(acct.month) + \". Balance \" + ledger.money(acct.balance) + \".\")\n    except ValueError as e:\n        print(\"Not done: \" + str(e) + \".\")\n\ndef withdraw(acct):\n    c = ask_amount(\"withdraw> \")\n    if c == -1:\n        print(\"Cancelled.\")\n        return\n    try:\n        acct.withdraw(c)\n        print(\"Withdrew \" + ledger.money(c) + \". Balance \" + ledger.money(acct.balance) + \".\")\n    except ValueError as e:\n        print(\"Refused: \" + str(e) + \"; the balance stays \" + ledger.money(acct.balance) + \".\")\n\ndef next_month(acct):\n    ending = month_label(acct.month)\n    earned = acct.next_month()\n    print(ending + \" closes: interest \" + ledger.money(earned) + \" at \" + rate_text(acct.rate) + \" a year. Balance \" + ledger.money(acct.balance) + \"; now \" + month_label(acct.month) + \".\")\n\ndef counted(n, one, many):\n    if n == 1:\n        return \"1 \" + one\n    return str(n) + \" \" + many\n\ndef padded(s, width):\n    while len(s) < width:\n        s = \" \" + s\n    return s\n\ndef statement(acct):\n    if len(acct.log) == 0:\n        print(\"Nothing has happened in the account yet.\")\n        return\n    print(\"month     entry           amount       balance\")\n    for e in acct.log:\n        kind = e[1]\n        while len(kind) < 9:\n            kind = kind + \" \"\n        amount = ledger.money(e[2])\n        if e[1] == \"withdraw\":\n            amount = \"-\" + amount\n        elif e[1] == \"refused\":\n            amount = \"(\" + amount + \")\"\n        print(month_label(e[0]) + \"   \" + kind + padded(amount, 13) + padded(ledger.money(e[3]), 14))\n\ndef check_books(acct):\n    b = ledger.books(acct.log)\n    print(\"From the log: deposits \" + ledger.money(b[0]) + \", withdrawals \" + ledger.money(b[1]) + \", interest \" + ledger.money(b[2]) + \".\")\n    print(\"They add up to \" + ledger.money(b[3]) + \"; the account says \" + ledger.money(acct.balance) + \".\")\n    refused = 0\n    for e in acct.log:\n        if e[1] == \"refused\":\n            refused = refused + 1\n    if b[3] == acct.balance and b[4] and b[5]:\n        print(\"The books agree, every balance on the way was at least 0.00, and \" + str(refused) + \" refused withdrawal(s) changed nothing.\")\n    else:\n        print(\"The books do NOT agree.\")\n\ndef goal(acct):\n    g = ask_amount(\"goal> \")\n    if g == -1:\n        print(\"Cancelled.\")\n        return\n    if g <= acct.balance:\n        print(\"The balance, \" + ledger.money(acct.balance) + \", is there already.\")\n        return\n    m = ask_amount(\"put in each month> \")\n    if m == -1:\n        print(\"Cancelled.\")\n        return\n    r = ledger.months_to_goal(acct.balance, acct.rate, m, g)\n    if r[0] == -1:\n        print(\"Putting in \" + ledger.money(m) + \" a month, it would take more than 100 years.\")\n        return\n    years = ledger.quotient(r[0], 12)\n    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.\")\n\ndef set_rate(acct):\n    while True:\n        answer = trim(input(\"yearly rate in % (0 to 20)> \"))\n        if answer == \"\":\n            print(\"Cancelled.\")\n            return\n        r = cents_of(answer, 2)\n        if r >= 0 and r <= 2000:\n            acct.rate = r\n            print(\"The rate is now \" + rate_text(r) + \" a year, paid monthly.\")\n            return\n        print(\"Type a rate like 2.5 or 3.25, from 0 to 20.\")\n\nprint(\"== Savings ledger ==\")\nprint(\"Whole cents, monthly interest, and a balance that never goes below zero.\")\nacct = ledger.Account(250)\nprint(\"The account opens in \" + month_label(0) + \" at \" + rate_text(250) + \" a year, paid monthly.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\")\n    choice = trim(input(\"choice> \"))\n    if choice == \"1\":\n        deposit(acct)\n    elif choice == \"2\":\n        withdraw(acct)\n    elif choice == \"3\":\n        next_month(acct)\n    elif choice == \"4\":\n        statement(acct)\n    elif choice == \"5\":\n        check_books(acct)\n    elif choice == \"6\":\n        goal(acct)\n    elif choice == \"7\":\n        set_rate(acct)\n    elif choice == \"8\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 8.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "ledger.eml",
      "eml": "# P064 savings ledger - the account, its log and the checks on it. Money is\n# whole cents; the yearly rate is in hundredths of a percent (250 = 2.50%).\n\ndef quotient(a, b):\n    # a // b for 0 <= a and b > 0. The numbers here stay far below 2^53, so a\n    # / b as a float is exact when the answer is whole, and otherwise at least\n    # 1/b away from a whole number - more than a float's error - so int()\n    # rounds down correctly.\n    return int(a / b)\n\ndef month_interest(balance, rate):\n    # One month at the yearly rate: balance x rate / 10000 / 12, to the\n    # nearest cent, halves up.\n    return quotient(2 * balance * rate + 120000, 240000)\n\nclass Account:\n    def __init__(self, rate):\n        0 => self.balance\n        rate => self.rate\n        0 => self.month\n        # each entry: [month, kind, amount, balance after]; kinds deposit,\n        # withdraw, interest, refused\n        [] => self.log\n\n    def deposit(self, cents):\n        if cents <= 0:\n            raise ValueError(\"a deposit must be more than 0.00\")\n        self.balance + cents => self.balance\n        self.log + [[self.month, \"deposit\", cents, self.balance]] => self.log\n\n    def withdraw(self, cents):\n        # The balance is never allowed below zero: a withdrawal that would do\n        # that is refused by raising, which the caller cannot overlook - the\n        # corpus case bank-account-invariant's point - and the refusal is\n        # logged without touching the balance.\n        if cents <= 0:\n            raise ValueError(\"a withdrawal must be more than 0.00\")\n        if cents > self.balance:\n            self.log + [[self.month, \"refused\", cents, self.balance]] => self.log\n            raise ValueError(\"only \" + money(self.balance) + \" is in the account\")\n        self.balance - cents => self.balance\n        self.log + [[self.month, \"withdraw\", cents, self.balance]] => self.log\n\n    def next_month(self):\n        # Interest for the month that ends, on the balance at its end.\n        month_interest(self.balance, self.rate) => earned\n        self.balance + earned => self.balance\n        self.log + [[self.month, \"interest\", earned, self.balance]] => self.log\n        self.month + 1 => self.month\n        return earned\n\ndef money(cents):\n    # 123456 -> \"1,234.56\"; negative with a minus in front.\n    \"\" => sign\n    cents => c\n    if c < 0:\n        \"-\" => sign\n        0 - c => c\n    str(c % 100) => part\n    if len(part) == 1:\n        \"0\" + part => part\n    str(quotient(c, 100)) => whole\n    \"\" => grouped\n    while len(whole) > 3:\n        \",\" + whole[len(whole) - 3:len(whole)] + grouped => grouped\n        whole[0:len(whole) - 3] => whole\n    return sign + whole + grouped + \".\" + part\n\ndef books(log):\n    # The checks of the corpus case bank-account-invariant, worked out from\n    # the log alone, not from the account: [deposits, withdrawals, interest,\n    # the balance they add up to, every running balance >= 0, every entry's\n    # balance equal to the one before plus its own change].\n    0 => dep\n    0 => wd\n    0 => intr\n    0 => running\n    True => never_negative\n    True => chain\n    for e in log:\n        if e[1] == \"deposit\":\n            dep + e[2] => dep\n            running + e[2] => running\n        elif e[1] == \"withdraw\":\n            wd + e[2] => wd\n            running - e[2] => running\n        elif e[1] == \"interest\":\n            intr + e[2] => intr\n            running + e[2] => running\n        if running < 0:\n            False => never_negative\n        if e[3] != running:\n            False => chain\n    return [dep, wd, intr, running, never_negative, chain]\n\ndef months_to_goal(balance, rate, monthly, goal):\n    # Months until the balance reaches goal, putting in `monthly` at the\n    # start of each month and earning that month's interest at its end; -1\n    # when it would take more than 100 years. Returns [months, balance then,\n    # interest earned on the way].\n    balance => b\n    0 => earned\n    0 => m\n    while b < goal:\n        if m == 1200:\n            return [-1, b, earned]\n        b + monthly => b\n        month_interest(b, rate) => i\n        b + i => b\n        earned + i => earned\n        m + 1 => m\n    return [m, b, earned]\n",
      "python": "def quotient(a, b):\n    return int(a / b)\n\ndef month_interest(balance, rate):\n    return quotient(2 * balance * rate + 120000, 240000)\n\nclass Account:\n    def __init__(self, rate):\n        self.balance = 0\n        self.rate = rate\n        self.month = 0\n        self.log = []\n    def deposit(self, cents):\n        if cents <= 0:\n            raise ValueError(\"a deposit must be more than 0.00\")\n        self.balance = self.balance + cents\n        self.log = self.log + [[self.month, \"deposit\", cents, self.balance]]\n    def withdraw(self, cents):\n        if cents <= 0:\n            raise ValueError(\"a withdrawal must be more than 0.00\")\n        if cents > self.balance:\n            self.log = self.log + [[self.month, \"refused\", cents, self.balance]]\n            raise ValueError(\"only \" + money(self.balance) + \" is in the account\")\n        self.balance = self.balance - cents\n        self.log = self.log + [[self.month, \"withdraw\", cents, self.balance]]\n    def next_month(self):\n        earned = month_interest(self.balance, self.rate)\n        self.balance = self.balance + earned\n        self.log = self.log + [[self.month, \"interest\", earned, self.balance]]\n        self.month = self.month + 1\n        return earned\n\ndef money(cents):\n    sign = \"\"\n    c = cents\n    if c < 0:\n        sign = \"-\"\n        c = 0 - c\n    part = str(c % 100)\n    if len(part) == 1:\n        part = \"0\" + part\n    whole = str(quotient(c, 100))\n    grouped = \"\"\n    while len(whole) > 3:\n        grouped = \",\" + whole[len(whole) - 3:len(whole)] + grouped\n        whole = whole[0:len(whole) - 3]\n    return sign + whole + grouped + \".\" + part\n\ndef books(log):\n    dep = 0\n    wd = 0\n    intr = 0\n    running = 0\n    never_negative = True\n    chain = True\n    for e in log:\n        if e[1] == \"deposit\":\n            dep = dep + e[2]\n            running = running + e[2]\n        elif e[1] == \"withdraw\":\n            wd = wd + e[2]\n            running = running - e[2]\n        elif e[1] == \"interest\":\n            intr = intr + e[2]\n            running = running + e[2]\n        if running < 0:\n            never_negative = False\n        if e[3] != running:\n            chain = False\n    return [dep, wd, intr, running, never_negative, chain]\n\ndef months_to_goal(balance, rate, monthly, goal):\n    b = balance\n    earned = 0\n    m = 0\n    while b < goal:\n        if m == 1200:\n            return [-1, b, earned]\n        b = b + monthly\n        i = month_interest(b, rate)\n        b = b + i\n        earned = earned + i\n        m = m + 1\n    return [m, b, earned]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n2\n50\n1\nabc\n-5\n12.345\n1000000.01\n0\n1\n1,500\n2\n2000\n7\n25\nx\n\n6\n100\n6\n5000\n\n7\n0\n6\n1,000,000\n1\n5\n4\n8\n",
      "screen": "== Savings ledger ==\nWhole cents, monthly interest, and a balance that never goes below zero.\nThe account opens in 2026-10 at 2.50% a year, paid monthly.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 0\nPick a number from 1 to 8.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> x\nPick a number from 1 to 8.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 2\nwithdraw> 50\nRefused: only 0.00 is in the account; the balance stays 0.00.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 1\ndeposit> abc\nType an amount like 250 or 12.50, up to 1,000,000.00.\ndeposit> -5\nType an amount like 250 or 12.50, up to 1,000,000.00.\ndeposit> 12.345\nType an amount like 250 or 12.50, up to 1,000,000.00.\ndeposit> 1000000.01\nType an amount like 250 or 12.50, up to 1,000,000.00.\ndeposit> 0\nNot done: a deposit must be more than 0.00.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 1\ndeposit> 1,500\nDeposited 1,500.00 in 2026-10. Balance 1,500.00.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 2\nwithdraw> 2000\nRefused: only 1,500.00 is in the account; the balance stays 1,500.00.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 7\nyearly rate in % (0 to 20)> 25\nType a rate like 2.5 or 3.25, from 0 to 20.\nyearly rate in % (0 to 20)> x\nType a rate like 2.5 or 3.25, from 0 to 20.\nyearly rate in % (0 to 20)> \nCancelled.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 6\ngoal> 100\nThe balance, 1,500.00, is there already.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 6\ngoal> 5000\nput in each month> \nCancelled.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 7\nyearly rate in % (0 to 20)> 0\nThe rate is now 0.00% a year, paid monthly.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 6\ngoal> 1,000,000\nput in each month> 1\nPutting in 1.00 a month, it would take more than 100 years.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 5\nFrom the log: deposits 1,500.00, withdrawals 0.00, interest 0.00.\nThey add up to 1,500.00; the account says 1,500.00.\nThe books agree, every balance on the way was at least 0.00, and 2 refused withdrawal(s) changed nothing.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 4\nmonth     entry           amount       balance\n2026-10   refused        (50.00)          0.00\n2026-10   deposit       1,500.00      1,500.00\n2026-10   refused     (2,000.00)      1,500.00\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 8\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n1000\n1\n250.50\n3\n2\n300\n3\n3\n4\n5\n6\n10,000\n500\n7\n4\n6\n10,000\n500\n8\n",
      "screen": "== Savings ledger ==\nWhole cents, monthly interest, and a balance that never goes below zero.\nThe account opens in 2026-10 at 2.50% a year, paid monthly.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 1\ndeposit> 1000\nDeposited 1,000.00 in 2026-10. Balance 1,000.00.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 1\ndeposit> 250.50\nDeposited 250.50 in 2026-10. Balance 1,250.50.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 3\n2026-10 closes: interest 2.61 at 2.50% a year. Balance 1,253.11; now 2026-11.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 2\nwithdraw> 300\nWithdrew 300.00. Balance 953.11.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 3\n2026-11 closes: interest 1.99 at 2.50% a year. Balance 955.10; now 2026-12.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 3\n2026-12 closes: interest 1.99 at 2.50% a year. Balance 957.09; now 2027-01.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 4\nmonth     entry           amount       balance\n2026-10   deposit       1,000.00      1,000.00\n2026-10   deposit         250.50      1,250.50\n2026-10   interest          2.61      1,253.11\n2026-11   withdraw       -300.00        953.11\n2026-11   interest          1.99        955.10\n2026-12   interest          1.99        957.09\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 5\nFrom the log: deposits 1,250.50, withdrawals 300.00, interest 6.59.\nThey add up to 957.09; the account says 957.09.\nThe books agree, every balance on the way was at least 0.00, and 0 refused withdrawal(s) changed nothing.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 6\ngoal> 10,000\nput in each month> 500\nPutting in 500.00 a month at 2.50%: 18 months (1 year 6 months), reaching 10,173.88 with 216.79 of it interest.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 7\nyearly rate in % (0 to 20)> 4\nThe rate is now 4.00% a year, paid monthly.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 6\ngoal> 10,000\nput in each month> 500\nPutting in 500.00 a month at 4.00%: 18 months (1 year 6 months), reaching 10,306.65 with 349.56 of it interest.\n\n1) deposit  2) withdraw  3) next month  4) statement  5) check the books  6) goal  7) rate  8) quit\nchoice> 8\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "bank-account-simulator",
      "caseId": "013-bank-account-simulator",
      "title": "Case corpus: a self-authored bank account simulator"
    },
    {
      "slug": "bank-account-invariant",
      "caseId": "126-bank-account-invariant",
      "title": "A class that defends an invariant"
    }
  ],
  "updated": "2026-10-14"
}
