{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-000",
   "metadata": {},
   "source": [
    "# Lesson 3 — Functional Programming & Core Math Applications\n",
    "\n",
    "**Companion notebook** for the Lesson 3 slides. The **Your turn** section builds an\n",
    "interest-theory toolkit. When your functions pass their checks, copy them into\n",
    "`src/foundations/interest.py` so every later lesson can import them."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-001",
   "metadata": {},
   "outputs": [],
   "source": [
    "def check(label, got, expected, tol=1e-6):\n",
    "    \"\"\"Compare your answer with the expected value; prints ✅ or ❌ (never raises).\"\"\"\n",
    "    import math\n",
    "    try:\n",
    "        if isinstance(expected, float) or isinstance(got, float):\n",
    "            ok = math.isclose(float(got), float(expected), rel_tol=tol, abs_tol=tol)\n",
    "        elif isinstance(expected, (list, tuple)) and expected and isinstance(expected[0], str):\n",
    "            ok = list(got) == list(expected)\n",
    "        elif hasattr(expected, \"__len__\") and not isinstance(expected, str):\n",
    "            import numpy as np\n",
    "            ok = np.allclose(np.asarray(got, dtype=float), np.asarray(expected, dtype=float), rtol=tol, atol=tol)\n",
    "        else:\n",
    "            ok = got == expected\n",
    "    except Exception:\n",
    "        ok = False\n",
    "    print((\"✅ \" if ok else \"❌ \") + f\"{label}: got {got!r}, expected {expected!r}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-002",
   "metadata": {},
   "source": [
    "## 1. Defining functions\n",
    "A function has a name, parameters, a docstring, and a `return`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-003",
   "metadata": {},
   "outputs": [],
   "source": [
    "def discount_factor(i, t):\n",
    "    \"\"\"Present value of 1 due at time t, at effective rate i.\"\"\"\n",
    "    return (1 + i) ** -t\n",
    "\n",
    "\n",
    "print(discount_factor(0.05, 10))\n",
    "help(discount_factor)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-004",
   "metadata": {},
   "source": [
    "## 2. Positional, keyword, and default arguments"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-005",
   "metadata": {},
   "outputs": [],
   "source": [
    "def pv(amount, rate=0.05, years=1):\n",
    "    return amount * (1 + rate) ** -years\n",
    "\n",
    "\n",
    "print(pv(1000), pv(1000, 0.03), pv(1000, years=10), pv(amount=1000, rate=0.03, years=10))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-006",
   "metadata": {},
   "source": [
    "### Never use a mutable default\n",
    "The default list is created **once**, so every call shares it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-007",
   "metadata": {},
   "outputs": [],
   "source": [
    "def add_claim_bad(amount, claims=[]):\n",
    "    claims.append(amount)\n",
    "    return claims\n",
    "\n",
    "\n",
    "print(add_claim_bad(1), add_claim_bad(2))   # the second call remembers the first!\n",
    "\n",
    "\n",
    "def add_claim(amount, claims=None):\n",
    "    if claims is None:\n",
    "        claims = []\n",
    "    claims.append(amount)\n",
    "    return claims\n",
    "\n",
    "\n",
    "print(add_claim(1), add_claim(2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-008",
   "metadata": {},
   "source": [
    "## 3. Scope: local names disappear when the function returns"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-009",
   "metadata": {},
   "outputs": [],
   "source": [
    "x = 10\n",
    "\n",
    "\n",
    "def f():\n",
    "    x = 5          # a new, local x\n",
    "    return x\n",
    "\n",
    "\n",
    "print(f(), x)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-010",
   "metadata": {},
   "source": [
    "## 4. Errors: raise them for bad input, catch only what you expect"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-011",
   "metadata": {},
   "outputs": [],
   "source": [
    "def safe_discount_factor(i: float, t: float) -> float:\n",
    "    \"\"\"Discount factor that rejects impossible interest rates.\"\"\"\n",
    "    if i <= -1:\n",
    "        raise ValueError(f\"interest rate must be > -1, got {i}\")\n",
    "    return (1 + i) ** -t\n",
    "\n",
    "\n",
    "try:\n",
    "    safe_discount_factor(-1.5, 3)\n",
    "except ValueError as err:\n",
    "    print(\"ValueError:\", err)\n",
    "\n",
    "\n",
    "def to_float(text: str) -> float | None:\n",
    "    \"\"\"Parse a number from a data file; None if impossible.\"\"\"\n",
    "    try:\n",
    "        return float(text.replace(\",\", \"\"))\n",
    "    except ValueError:\n",
    "        return None\n",
    "\n",
    "\n",
    "print(to_float(\"1,200\"), to_float(\"N/A\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-012",
   "metadata": {},
   "source": [
    "## 5. Comprehensions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-013",
   "metadata": {},
   "outputs": [],
   "source": [
    "claims = [1200, 350, 8800, 40, 15000]\n",
    "print([round(discount_factor(0.05, t), 4) for t in range(1, 6)])\n",
    "print([c for c in claims if c > 1000])\n",
    "print([min(c, 10_000) for c in claims])\n",
    "print({t: round(discount_factor(0.05, t), 4) for t in (1, 5, 10)})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-014",
   "metadata": {},
   "source": [
    "---\n",
    "## Your turn — an interest-theory toolkit\n",
    "\n",
    "Write each function below: pure, typed, documented. Reject impossible inputs with\n",
    "`ValueError`, and handle $i = 0$ in the annuities.\n",
    "\n",
    "| Function | Formula |\n",
    "|---|---|\n",
    "| `effective_from_nominal(i_m, m)` | $(1 + i^{(m)}/m)^m - 1$ |\n",
    "| `nominal_from_effective(i, m)` | $m[(1+i)^{1/m} - 1]$ |\n",
    "| `discount_rate(i)` | $d = i/(1+i)$ |\n",
    "| `force_of_interest(i)` | $\\delta = \\ln(1+i)$ |\n",
    "| `annuity_immediate(n, i)` | $(1 - v^n)/i$ |\n",
    "| `annuity_due(n, i)` | $(1 - v^n)/d$ |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-015",
   "metadata": {},
   "outputs": [],
   "source": [
    "def effective_from_nominal(i_m: float, m: int) -> float:\n",
    "    \"\"\"Effective annual rate from a nominal rate convertible m times a year.\"\"\"\n",
    "    ...  # your code here\n",
    "\n",
    "\n",
    "def nominal_from_effective(i: float, m: int) -> float:\n",
    "    \"\"\"Nominal rate convertible m times a year from an effective annual rate.\"\"\"\n",
    "    ...  # your code here\n",
    "\n",
    "\n",
    "def discount_rate(i: float) -> float:\n",
    "    \"\"\"Effective rate of discount d = i / (1 + i).\"\"\"\n",
    "    ...  # your code here\n",
    "\n",
    "\n",
    "def force_of_interest(i: float) -> float:\n",
    "    \"\"\"Force of interest delta = ln(1 + i).\"\"\"\n",
    "    ...  # your code here\n",
    "\n",
    "\n",
    "def annuity_immediate(n: int, i: float) -> float:\n",
    "    \"\"\"Present value of 1 paid at the end of each of n periods.\"\"\"\n",
    "    ...  # your code here\n",
    "\n",
    "\n",
    "def annuity_due(n: int, i: float) -> float:\n",
    "    \"\"\"Present value of 1 paid at the start of each of n periods.\"\"\"\n",
    "    ...  # your code here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-016",
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "check(\"effective from 6% monthly\", effective_from_nominal(0.06, 12), 0.06167781186449828)\n",
    "check(\"nominal (m=12) from 5%\", nominal_from_effective(0.05, 12), 0.04888948540378024)\n",
    "check(\"discount rate at 5%\", discount_rate(0.05), 0.047619047619047616)\n",
    "check(\"force of interest at 5%\", force_of_interest(0.05), 0.04879016416943201)\n",
    "check(\"annuity-immediate n=10\", annuity_immediate(10, 0.05), 7.721734929184818)\n",
    "check(\"annuity-due n=10\", annuity_due(10, 0.05), 8.10782167564406)\n",
    "check(\"i = 0 annuity\", annuity_immediate(10, 0.0), 10.0)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-017",
   "metadata": {},
   "source": [
    "### Identity checks — with a tolerance, never `==`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-018",
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    back = effective_from_nominal(nominal_from_effective(0.05, 12), 12)\n",
    "    print(\"round trip:\", back, \"| == 0.05?\", back == 0.05, \"| isclose?\", math.isclose(back, 0.05))\n",
    "    check(\"ä = (1+i)·a\", annuity_due(10, 0.05), 1.05 * annuity_immediate(10, 0.05))\n",
    "    check(\"d = 1 - v\", discount_rate(0.05), 1 - 1 / 1.05)\n",
    "except TypeError:\n",
    "    print(\"Finish the toolkit functions above, then rerun this cell.\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
