{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Floating point\n",
    "\n",
    "This is the notebook that explains the single most reported \"bug\" in every programming language: `0.1 + 0.2` doesn't equal `0.3`. It isn't a bug, and it isn't specific to Python — it's how binary floating point works everywhere. Once you understand *why*, the practical rules (don't compare floats with `==`, use `Decimal` for money) stop feeling like superstition."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The famous example"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "print(0.1 + 0.2)               # 0.30000000000000004\n",
    "print(0.1 + 0.2 == 0.3)        # False"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "That trailing `...04` is real, and `==` sees it. So the equality is genuinely false. To understand where it comes from, look at what `0.1` actually is once stored."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why: decimal fractions don't fit in binary\n",
    "\n",
    "A `float` stores numbers in **binary** fractions — sums of halves, quarters, eighths, and so on. Some decimals land exactly on such a sum (`0.5` is `1/2`, `0.25` is `1/4`). Most don't. `0.1` is a repeating fraction in binary, just as `1/3` is the repeating `0.333...` in decimal — so it gets stored as the *nearest representable value*, which is very slightly off.\n",
    "\n",
    "Ask Python to print `0.1` to 17 decimal places and the discrepancy appears:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "print(format(0.1, '.17f'))     # 0.10000000000000001 — not exactly 0.1\n",
    "print(format(0.3, '.17f'))     # 0.29999999999999999\n",
    "print(format(0.1 + 0.2, '.17f'))  # 0.30000000000000004"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The normal `print(0.1)` shows `0.1` because Python rounds to the shortest string that round-trips back to the same float. The error is still there; it's just hidden until an operation (like adding) makes it big enough to surface in the short form."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## What a float can represent\n",
    "\n",
    "A Python `float` is an IEEE 754 **double**: 64 bits, giving about **15–17 significant decimal digits** of precision and a range up to roughly 1.8 × 10³⁰⁸. That's plenty for measurements, physics, graphics, and statistics. What it can't do is represent most decimal fractions *exactly* — which is exactly what money needs. The [how floats work](https://agilearn.co.uk/guides/numbers-and-maths/concepts/how-floats-work) essay unpacks the bit layout; the practical upshot is the precision figure."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import sys\n",
    "print(sys.float_info.dig)      # 15 — guaranteed significant decimal digits\n",
    "print(sys.float_info.max)      # ~1.7976931348623157e+308"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comparing floats: never `==`\n",
    "\n",
    "Because results carry tiny errors, testing two floats for exact equality is unreliable. Instead ask whether they're *close enough*, with `math.isclose`. It handles the tolerance for you (relative by default, with an optional absolute tolerance for values near zero)."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "print(0.1 + 0.2 == 0.3)                    # False — don't do this\n",
    "print(math.isclose(0.1 + 0.2, 0.3))        # True — do this\n",
    "\n",
    "# near zero, give an absolute tolerance too:\n",
    "print(math.isclose(1e-10, 0.0))                       # False (relative tol fails at 0)\n",
    "print(math.isclose(1e-10, 0.0, abs_tol=1e-9))         # True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The surprise in `round`\n",
    "\n",
    "`round` uses **banker's rounding** (round half to *even*): a value exactly halfway goes to the nearest even digit, not always up. So `round(0.5)` is `0`, `round(2.5)` is `2`, but `round(3.5)` is `4`. This reduces statistical bias when you round lots of numbers, but it catches everyone the first time."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "print(round(0.5), round(1.5), round(2.5), round(3.5))   # 0 2 2 4\n",
    "\n",
    "# And rounding to decimal places can surprise you because of representation:\n",
    "print(round(2.675, 2))         # 2.67, not 2.68 — 2.675 is stored as 2.6749999..."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you need the \"always round half up\" rule most people expect — especially for money — `round` is the wrong tool. Use `Decimal` with an explicit rounding mode, covered in the [rounding recipe](https://agilearn.co.uk/guides/numbers-and-maths/recipes/round-numbers-correctly) and the [Decimal notebook](https://agilearn.co.uk/guides/numbers-and-maths/learn/03-decimal-and-fraction)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Infinity and not-a-number\n",
    "\n",
    "Floats include two special values. **Infinity** (`inf`) is what you get from overflow or an explicit `float('inf')`. **Not-a-Number** (`nan`) represents an undefined result like `inf - inf` or `0/0`-style operations. Both propagate through arithmetic."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "print(math.inf, -math.inf)         # inf -inf\n",
    "print(1e308 * 10)                  # inf — overflow, not an error\n",
    "print(math.inf - math.inf)         # nan — undefined\n",
    "print(float('nan'))                # nan"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`nan` has a property that trips people up: **it is not equal to anything, including itself.** So you can't test for it with `==`; use `math.isnan`. (This is by design — it's how IEEE 754 lets a `nan` signal \"no valid answer\".)"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "nan = float('nan')\n",
    "print(nan == nan)              # False (!)\n",
    "print(nan == 0, nan < 1)       # False False — all comparisons are False\n",
    "print(math.isnan(nan))         # True — the correct way to test\n",
    "print(math.isinf(math.inf))    # True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A nasty consequence: a `nan` hiding in a list breaks sorting and `min`/`max` in confusing ways, and `nan in [nan]` can be `True` (because membership tests identity first). If your data might contain `nan`, filter it out early with `math.isnan`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## So when is `float` the right choice?\n",
    "\n",
    "Most of the time. Use `float` for anything measured or continuous — physical quantities, coordinates, percentages, scientific computation, statistics — where a relative error around the 16th digit is irrelevant. Avoid it when values must be *exact*: money, and anything where rounding has to follow a specific published rule. For those, reach for `Decimal` ([next notebook](https://agilearn.co.uk/guides/numbers-and-maths/learn/03-decimal-and-fraction))."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Recap\n",
    "\n",
    "- `float` stores binary fractions, so most decimals (like `0.1`) are tiny approximations — this is IEEE 754, not a Python quirk.\n",
    "- A double gives ~15–17 significant digits and a huge range.\n",
    "- Compare with `math.isclose`, never `==`.\n",
    "- `round` does banker's rounding (half to even); `round(2.5) == 2`.\n",
    "- `inf` comes from overflow; `nan` is never equal to anything — test with `math.isnan`.\n",
    "- Use `float` for measured quantities; use `Decimal` when exactness matters.\n",
    "\n",
    "Next: [Decimal and Fraction](https://agilearn.co.uk/guides/numbers-and-maths/learn/03-decimal-and-fraction) — the exact types, and the one rule that makes `Decimal` actually work."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}