"""Reading a printed figure as a number. The only place that does it.

A figure printed in a filing carries a currency sign, thousands separators, a
percent sign, a trailing multiple, or parentheses standing in for a minus. Every
consumer of an answer wants the number underneath: a spreadsheet column, a
comparison against a gold value, an arithmetic check. Each of those wanting its
own conversion is how the same figure comes out with two different values, and
the way they differ is by sign.

So there is one function, and everything that needs a number calls it. It is not
a model's job either: the conversion is deterministic, and a model asked for it
alongside the figure returns the figure reliably and the number only most of the
time, which is a hole in a column no one can predict.
"""

from __future__ import annotations

import re
from decimal import Decimal, InvalidOperation
from typing import Optional


def norm_printed(s: Optional[str]) -> str:
    """Trim and collapse whitespace, for comparing two printed figures."""
    return re.sub(r"\s+", " ", (s or "").strip())


def as_number(s: Optional[str]) -> Optional[Decimal]:
    """The numeric value of a printed figure, or None if it is not numeric.

    Parentheses mean negative, as financial statements print them. Currency
    signs, thousands separators, percent signs and a trailing multiple 'x' are
    dropped; a figure carrying a scale word keeps only its digits, so
    '$1.2 million' reads as 1.2 and is compared against a gold that prints the
    same way.

    A percentage keeps the magnitude it was printed with: '92.6%' is 92.6, not
    0.926. Nothing here rescales a figure, because a reader checking a row
    against the filing is comparing it to what the page says.
    """
    if not s:
        return None
    # Currency, separators and spaces come off before the parentheses are read:
    # a cell prints "$ (10,550)", so a parenthesis test applied first would see
    # a string starting with "$" and miss the negative entirely.
    t = re.sub(r"[$,%\s]", "", norm_printed(s))
    neg = t.startswith("(") and t.endswith(")")
    t = t.strip("()")
    t = re.sub(r"[xX]$", "", t)
    t = re.sub(r"(?i)(million|billion|thousand|mm|bn|[mbk])$", "", t)
    if not re.match(r"^-?\d*\.?\d+$", t):
        return None
    try:
        v = Decimal(t)
    except InvalidOperation:
        return None
    return -v if neg else v


def is_percent(s: Optional[str]) -> bool:
    """Whether a printed figure is a percentage.

    A spreadsheet given '92.6%' stores the fraction and formats the cell as a
    percentage, so the figure both computes as a rate and displays as the
    document printed it. Given a bare 92.6 it stores ninety-two point six, which
    is not the rate. So an export has to know which figures keep their sign, and
    this is the test — a question about notation, never about magnitude.
    """
    return norm_printed(s).endswith("%")
