Skip to content

Percentage Calculator — Python source

Calculate percentages three ways - X% of Y, X is what percent of Y, and the percentage change between two values. Runs entirely in your browser, with a shareable link.

This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.

"""percentage-calculator — Python port.

CosmoDev polyglot showcase port of the ``percentage-calculator`` tool. Pure
percentage logic ported from ``src/lib/percentage.ts``. Deterministic and
side-effect free: every function returns ``None`` for non-finite input or an
undefined result (a zero divisor) instead of raising, and rounds to a
configurable maximum number of decimal places.

Display source — part of CosmoDev's polyglot tool pages.
"""

from __future__ import annotations

import math
import sys
from dataclasses import dataclass
from typing import Optional

DEFAULT_MAX_DECIMALS: int = 2

# Equivalent to TypeScript's ``Number.EPSILON``: the gap between 1.0 and the
# next representable IEEE-754 double (2^-52 ≈ 2.22e-16). Python's ``math``
# module does not export this constant, so we read it from ``sys.float_info``.
_EPSILON: float = sys.float_info.epsilon


@dataclass(frozen=True)
class PercentOptions:
    """Options governing result precision.

    ``max_decimals`` defaults to 2 when ``None``, mirroring the TypeScript
    ``PercentOptions`` optional field — which (unlike a plain ``int``)
    distinguishes "unset" from an explicit ``0``.
    """

    max_decimals: Optional[int] = None


def _every_finite(*values: float) -> bool:
    """Report whether every value is finite.

    ``NaN`` and ±inf are treated as invalid inputs throughout this module:
    callers receive ``None`` rather than a propagated ``NaN``.
    """
    return all(math.isfinite(v) for v in values)


def _resolve_decimals(opts: PercentOptions) -> int:
    """Return the effective precision, falling back to the default when unset."""
    if opts.max_decimals is not None:
        return opts.max_decimals
    return DEFAULT_MAX_DECIMALS


def _js_round(value: float) -> float:
    """Replicate JavaScript's ``Math.round``: round half toward +infinity.

    Python's built-in ``round()`` uses banker's rounding (half to even), which
    diverges from the TypeScript source on exact ``.5`` ties. Combined with the
    epsilon nudge applied by :func:`round_value`, this keeps the port
    consistent with ``Math.round`` for every realistic percentage input.
    """
    return math.floor(value + 0.5)


def round_value(n: float, max_decimals: int = DEFAULT_MAX_DECIMALS) -> float:
    """Round ``n`` to at most ``max_decimals`` places (default 2).

    Adding the machine epsilon before scaling absorbs the tiny errors that
    arise from representing decimal fractions in binary floating point (the
    classic ``0.1 + 0.2 == 0.30000000000000004``). Non-finite values are
    returned unchanged so this function is total.
    """
    if not math.isfinite(n):
        return n
    factor = 10 ** max_decimals
    return _js_round((n + _EPSILON) * factor) / factor


def percent_of(
    pct: float, value: float, opts: Optional[PercentOptions] = None
) -> Optional[float]:
    """X% of ``value``: ``(pct / 100) * value``.

    Returns ``None`` when either input is non-finite.
    """
    if opts is None:
        opts = PercentOptions()
    if not _every_finite(pct, value):
        return None
    return round_value((pct / 100) * value, _resolve_decimals(opts))


def what_percent(
    part: float, total: float, opts: Optional[PercentOptions] = None
) -> Optional[float]:
    """What percentage ``part`` is of ``total``: ``(part / total) * 100``.

    Returns ``None`` when ``total`` is zero (the ratio is undefined) or either
    input is non-finite.
    """
    if opts is None:
        opts = PercentOptions()
    if not _every_finite(part, total):
        return None
    if total == 0:
        return None
    return round_value((part / total) * 100, _resolve_decimals(opts))


def percent_change(
    from_: float, to: float, opts: Optional[PercentOptions] = None
) -> Optional[float]:
    """Percentage change from ``from_`` to ``to``: ``((to - from_) / abs(from_)) * 100``.

    The denominator is absolute so the result's sign reflects only the
    direction of change (positive for an increase, negative for a decrease).
    Returns ``None`` when ``from_`` is zero (no meaningful base to compare
    against) or either input is non-finite.

    The trailing underscore on ``from_`` avoids shadowing the Python keyword
    ``from``; it maps to the TypeScript parameter named ``from``.
    """
    if opts is None:
        opts = PercentOptions()
    if not _every_finite(from_, to):
        return None
    if from_ == 0:
        return None
    return round_value(((to - from_) / abs(from_)) * 100, _resolve_decimals(opts))

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →