{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Round numbers correctly\n",
    "\n",
    "**The question.** You need to round a number — to a whole number, to two decimal places, to a few significant figures, or to the nearest penny — and you want the result your users expect, not a surprise from `round`.\n",
    "\n",
    "The answer depends on *which* rounding you mean. The built-in `round` uses banker's rounding (half to even), which is right for statistics but wrong for the \"always round .5 up\" most people expect. Here's each case and the tool that fits it."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The default: `round` rounds half to *even*\n",
    "\n",
    "`round` sends a value exactly halfway to the nearest **even** digit. Over many values this avoids the upward bias of always-round-up, but it means `round(0.5)` is `0` and `round(2.5)` is `2`."
   ]
  },
  {
   "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",
    "print(round(2.675, 2))   # 2.67 — and 2.675 is really 2.67499... as a float"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## \"Round half up\": use `Decimal.quantize`\n",
    "\n",
    "For the schoolbook rule — halves always go up — round a `Decimal` with `ROUND_HALF_UP`. Build the `Decimal` from a **string** so the value is exact before you round it."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from decimal import Decimal, ROUND_HALF_UP\n",
    "\n",
    "def round_half_up(value, places=0):\n",
    "    q = Decimal(1).scaleb(-places)            # e.g. places=2 -> Decimal('0.01')\n",
    "    return Decimal(str(value)).quantize(q, rounding=ROUND_HALF_UP)\n",
    "\n",
    "print(round_half_up(0.5))         # 1\n",
    "print(round_half_up(2.5))         # 3\n",
    "print(round_half_up(2.675, 2))    # 2.68"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Round to decimal places for *display*\n",
    "\n",
    "If you only need a rounded number *shown* to the user, don't round the value at all — format it. An f-string with `.2f` gives exactly two decimal places as text, sidesteps representation surprises, and keeps your underlying number intact."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "x = 3.14159\n",
    "print(f'{x:.2f}')          # '3.14'\n",
    "print(f'{1/3:.4f}')        # '0.3333'\n",
    "print(f'{2.5:.0f}')        # '2'  (note: format also rounds half to even)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Rounding for display (format) versus rounding the value (so later maths uses the rounded number) are different jobs — pick format when it's purely for output."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Round to significant figures\n",
    "\n",
    "Neither `round` nor `.Nf` does significant figures directly. For a quick numeric result, this helper works; for display, the `g` format type rounds to significant figures and trims trailing zeros."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from math import log10, floor\n",
    "\n",
    "def round_sig(x, sig):\n",
    "    if x == 0:\n",
    "        return 0.0\n",
    "    return round(x, -int(floor(log10(abs(x)))) + (sig - 1))\n",
    "\n",
    "print(round_sig(12345, 2))       # 12000  (int in -> int out)\n",
    "print(round_sig(0.0034567, 2))   # 0.0035\n",
    "\n",
    "# for display, the 'g' type does significant figures:\n",
    "print(f'{12345:.2g}')            # '1.2e+04'\n",
    "print(f'{0.0034567:.2g}')        # '0.0035'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Round money to the penny\n",
    "\n",
    "Money is the case where rounding *and* exactness both matter, so do it in `Decimal`. Keep everything decimal, then `quantize` to two places with `ROUND_HALF_UP` at the point you need a final figure."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from decimal import Decimal, ROUND_HALF_UP\n",
    "\n",
    "pennies = Decimal('0.01')\n",
    "subtotal = Decimal('19.99') * 3        # 59.97 exactly\n",
    "with_tax = subtotal * Decimal('1.20')  # 20% VAT -> 71.964\n",
    "total = with_tax.quantize(pennies, rounding=ROUND_HALF_UP)\n",
    "print(total)                            # 71.96"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The full money workflow is in [handle money with Decimal](https://agilearn.co.uk/guides/numbers-and-maths/recipes/handle-money-with-decimal)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Which to use\n",
    "\n",
    "| You want… | Use |\n",
    "| --- | --- |\n",
    "| statistically unbiased rounding | built-in `round` (half to even) |\n",
    "| \"halves go up\" | `Decimal.quantize(..., ROUND_HALF_UP)` |\n",
    "| a rounded number just for display | an f-string: `f'{x:.2f}'` |\n",
    "| significant figures for display | the `g` type: `f'{x:.3g}'` |\n",
    "| money | `Decimal` throughout, `quantize` at the end |"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}