{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Decimal and Fraction\n",
    "\n",
    "The [last notebook](https://agilearn.co.uk/guides/numbers-and-maths/learn/02-floating-point) showed that `float` can't represent most decimals exactly. When *exactness* matters — money, above all — Python's standard library has two types that don't approximate: `Decimal` for exact decimal arithmetic, and `Fraction` for exact rational numbers. This notebook covers both, and the one rule that makes `Decimal` actually deliver on its promise."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `Decimal`: exact decimal arithmetic\n",
    "\n",
    "`Decimal` represents numbers in base 10, the way you'd write them on paper, so `0.1` really is one tenth. The famous failing sum just works:"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from decimal import Decimal\n",
    "\n",
    "print(Decimal('0.1') + Decimal('0.2'))            # 0.3 — exactly\n",
    "print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3'))   # True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The one rule: build from strings, not floats\n",
    "\n",
    "This is the mistake that quietly defeats `Decimal`. If you write `Decimal(0.1)`, you pass the *already-broken* float `0.1` into the constructor — and `Decimal` faithfully preserves every wrong digit of it. Always construct from a **string** (or an `int`), so the value is exact from the start."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# WRONG: the float 0.1 is already an approximation before Decimal sees it\n",
    "print(Decimal(0.1))     # 0.1000000000000000055511151231257827021181583404541015625\n",
    "\n",
    "# RIGHT: a string is interpreted exactly\n",
    "print(Decimal('0.1'))   # 0.1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`Decimal(0.1)` isn't an error — it's occasionally even what you want (to see exactly what a float holds). But for money and any decimal you typed or read as text, the string form is the only safe one."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Precision and context\n",
    "\n",
    "`Decimal` arithmetic happens within a **context** that sets the precision — the number of *significant digits* kept, 28 by default. So division that doesn't terminate is rounded to 28 significant figures, not stored forever."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from decimal import Decimal, getcontext\n",
    "\n",
    "print(getcontext().prec)            # 28 — significant digits by default\n",
    "print(Decimal(1) / Decimal(3))      # 0.3333333333333333333333333333 (28 threes)\n",
    "\n",
    "getcontext().prec = 6               # narrow the context\n",
    "print(Decimal(1) / Decimal(3))      # 0.333333\n",
    "getcontext().prec = 28              # restore the default for later cells"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that precision is *significant digits*, not decimal places. To pin a value to a fixed number of decimal places — like pence — you use `quantize`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Rounding to the penny with `quantize`\n",
    "\n",
    "`quantize` rounds a `Decimal` to the same number of decimal places as a template you give it (`Decimal('0.01')` for two places). It also lets you choose the **rounding mode** — and this is where you fix the banker's-rounding surprise from the last notebook. For money, people usually want `ROUND_HALF_UP`."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN\n",
    "\n",
    "price = Decimal('0.125')\n",
    "print(price.quantize(Decimal('0.01')))                          # 0.12 (default: half to even)\n",
    "print(price.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)) # 0.12 — 2 is even\n",
    "print(price.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))   # 0.13 — the 'expected' rule"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The full money pattern — parse, compute, then `quantize` once at the end — is laid out in the [handle money with Decimal](https://agilearn.co.uk/guides/numbers-and-maths/recipes/handle-money-with-decimal) recipe."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Don't mix `Decimal` and `float`\n",
    "\n",
    "You can add two `Decimal`s, or a `Decimal` and an `int`, but mixing a `Decimal` with a `float` raises `TypeError` — deliberately, because combining an exact value with an approximate one defeats the purpose. Keep a calculation entirely in `Decimal` from input to output."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from decimal import Decimal\n",
    "\n",
    "print(Decimal('1.50') + 1)          # 2.50 — int is fine\n",
    "try:\n",
    "    Decimal('1.50') + 0.1           # float is not\n",
    "except TypeError as exc:\n",
    "    print('TypeError:', exc)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `Fraction`: exact rational numbers\n",
    "\n",
    "Where `Decimal` is exact in base 10, `Fraction` is exact for any *ratio* of integers — it stores a numerator and denominator and keeps them reduced. So `1/3` is held precisely as `1/3`, and thirds add up the way they should."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from fractions import Fraction\n",
    "\n",
    "print(Fraction(1, 3) + Fraction(1, 6))     # 1/2 — exact, and auto-reduced\n",
    "print(Fraction(1, 3) + Fraction(1, 3) + Fraction(1, 3))   # 1\n",
    "print(Fraction(2, 4))                       # 1/2 — reduced on construction"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The same string-versus-float rule applies: `Fraction('0.1')` is exactly `1/10`, while `Fraction(0.1)` captures the messy binary float. `limit_denominator` is the escape hatch — it finds the simplest fraction close to a float, which is great for recovering the \"intended\" value."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from fractions import Fraction\n",
    "\n",
    "print(Fraction('0.1'))                      # 1/10 — exact from a string\n",
    "print(Fraction(0.1))                        # 3602879701896397/36028797018963968\n",
    "print(Fraction(0.1).limit_denominator(1000))  # 1/10 — recovered\n",
    "\n",
    "print(float(Fraction(1, 3)))                # 0.3333333333333333 — convert out when needed"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Which exact type, when?\n",
    "\n",
    "- **`Decimal`** — money and anything specified in decimal places with defined rounding. It thinks in tenths and hundredths, like an accountant.\n",
    "- **`Fraction`** — exact ratios and rational arithmetic where thirds, sevenths, and the like must stay exact: probabilities, exact slopes, symbolic-ish maths.\n",
    "- **`float`** — everything measured or continuous, where speed matters and a 16th-digit error doesn't.\n",
    "\n",
    "Both exact types are slower than `float` and most arithmetic doesn't need them — but when correctness to the penny or the exact ratio matters, they're the difference between right and *almost* right. The [choosing a numeric type](https://agilearn.co.uk/guides/numbers-and-maths/concepts/choosing-a-numeric-type) essay turns this into a decision you can make quickly."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Recap\n",
    "\n",
    "- `Decimal` is exact in base 10 — `Decimal('0.1') + Decimal('0.2') == Decimal('0.3')`.\n",
    "- **Always build `Decimal`/`Fraction` from strings**, not floats, or you import the float's error.\n",
    "- Precision is significant digits (28 by default); `quantize` pins decimal places and sets the rounding mode (`ROUND_HALF_UP` for money).\n",
    "- Don't mix `Decimal` and `float` — it raises `TypeError` on purpose.\n",
    "- `Fraction` keeps exact ratios, auto-reduced; `limit_denominator` recovers tidy values from floats.\n",
    "\n",
    "Next: [Maths, statistics, and random](https://agilearn.co.uk/guides/numbers-and-maths/learn/04-math-random-statistics) — the standard-library toolkit for actually computing things."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}