Module inkfill.numerals

Numeral systems.

Expand source code
"""[Numeral systems](https://en.wikipedia.org/wiki/Numeral_system)."""

# std
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
from typing import Dict

# pkg
from .registry import Registrable


def commafy(num: int) -> str:
    """Return a comma-grouped number."""
    return f"{num:,}"


def to_decimal(num: int) -> str:
    """Decimal numerals."""
    # https://en.wikipedia.org/wiki/Decimal
    return str(num)


## Alphabetic numerals

ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"""English alphabet."""


def to_alpha(num: int) -> str:
    """English alphabetical numerals."""
    # https://en.wikipedia.org/wiki/English_alphabet
    result = ""
    if num < 1:
        return result

    while num > 0:
        num, remainder = divmod(num - 1, 26)
        result = ALPHABET[remainder] + result

    return result


ROMAN_VALS = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
"""Roman numeral values."""

ROMAN_SYMS = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]
"""Roman numeral symbols."""


def to_roman(num: int) -> str:
    """Return the [Roman numeral](https://en.wikipedia.org/wiki/Roman_numerals)."""
    result = ""
    if num < 1:
        return result

    idx = 0
    while num > 0:
        for _ in range(num // ROMAN_VALS[idx]):
            result += ROMAN_SYMS[idx]
            num -= ROMAN_VALS[idx]
        idx += 1
    return result


## [English numerals](https://en.wikipedia.org/wiki/English_numerals)

NAME_ONES: Dict[int, str] = {
    0: "",
    1: "one",
    2: "two",
    3: "three",
    4: "four",
    5: "five",
    6: "six",
    7: "seven",
    8: "eight",
    9: "nine",
    10: "ten",
    11: "eleven",
    12: "twelve",
    13: "thirteen",
    14: "fourteen",
    15: "fifteen",
    16: "sixteen",
    17: "seventeen",
    18: "eighteen",
    19: "nineteen",
}
"""Name of numbers under 20."""

NAME_TENS: Dict[int, str] = {
    2: "twenty",
    3: "thirty",
    4: "forty",
    5: "fifty",
    6: "sixty",
    7: "seventy",
    8: "eighty",
    9: "ninety",
}
"""Name of the tens."""

NAME_ILLIONS: Dict[int, str] = {
    1: "thousand",
    2: "million",
    3: "billion",
    4: "trillion",
    5: "quadrillion",
    6: "quintillion",
    7: "sextillion",
    8: "septillion",
    9: "octillion",
    10: "nonillion",
    11: "decillion",
}
"""Names of the larger numbers."""

ORDINAL_SUFFIXES = {
    "one": "first",
    "two": "second",
    "three": "third",
    "ve": "fth",  # five => fifth; twelve => twelfth
    "t": "th",  # eight => eighth
    "e": "th",  # nine => ninth
    "y": "ieth",  # twenty => twentieth
}
"""Special ordinal suffix replacements."""


def _space_join(*args: str) -> str:
    """Join non-empty arguments with a space."""
    return " ".join(arg for arg in args if arg)


def _divide(dividend: int, divisor: int, magnitude: str) -> str:
    return _space_join(_pos(dividend // divisor), magnitude, _pos(dividend % divisor))


def _pos(num: int) -> str:
    if num < 20:
        return NAME_ONES[num]
    if num < 100:
        tens = NAME_TENS[num // 10]
        ones = NAME_ONES[num % 10]
        if tens and ones:  # hyphenate numbers 21-99
            return f"{tens}-{ones}"
        return _space_join(tens, ones)
    if num < 1000:
        return _divide(num, 100, "hundred")

    illions_number, illions_name = 1, "thousand"
    for illions_number, illions_name in NAME_ILLIONS.items():
        if num < 1000 ** (illions_number + 1):
            break
    return _divide(num, 1000**illions_number, illions_name)


def to_cardinal(num: int) -> str:
    """English cardinal numerals."""
    # https://en.wikipedia.org/wiki/Cardinal_numeral
    # See: https://stackoverflow.com/a/54018199
    if num == 0:
        return "zero"
    if num < 0:
        return _space_join("negative", _pos(-num))
    return _pos(num)


def to_nth(num: int) -> str:
    """Numeric ordinal numerals ("1st", "2nd")."""
    # https://en.wikipedia.org/wiki/Ordinal_numeral
    if 10 <= num % 100 <= 20:
        suffix = "th"
    else:
        suffixes = {1: "st", 2: "nd", 3: "rd"}
        suffix = suffixes.get(num % 10, "th")
    return str(num) + suffix


def to_ordinal(num: int) -> str:
    """English ordinal numerals ("first", "second")."""
    # https://en.wikipedia.org/wiki/Ordinal_numeral
    cardinal = to_cardinal(num)
    for check, suffix in ORDINAL_SUFFIXES.items():
        if cardinal.endswith(check):
            return cardinal[: -len(check)] + suffix
    return cardinal + "th"


NUMERAL_FUNC = Callable[[int], str]
"""Render a numeral from an integer."""


@dataclass
class NumFormat(Registrable):
    """Numeral formatter."""

    name: str
    """Numeral system name."""

    numeral: NUMERAL_FUNC
    """Function that converts an integer to a numeral."""

    prefix: str = ""
    """Prefix string when rendering."""

    suffix: str = ""
    """Suffix string when rendering."""

    def render(self, num: int, punctuation: bool = False) -> str:
        """Render the numeral."""
        val = self.numeral(num)
        return val if not punctuation else f"{self.prefix}{val}{self.suffix}"

    __call__ = render


def call_lower(f: NUMERAL_FUNC) -> NUMERAL_FUNC:
    """Call `.lower()` after calling a render function."""
    return lambda n: f(n).lower()


def call_title(f: NUMERAL_FUNC) -> NUMERAL_FUNC:
    """Call `.title()` after calling a render function."""
    return lambda n: f(n).title()


NULL_FORMAT = NumFormat("", lambda *_: "").add()
"""Special empty formatting."""

NumFormat.DEFAULT = DECIMAL = NumFormat("decimal", to_decimal, prefix=".").add()
"""Arabic numerals: 1, 2, 3..."""
# https://www.w3.org/TR/predefined-counter-styles/#decimal

LOWER_ALPHA = NumFormat(
    "lower-alpha", call_lower(to_alpha), prefix="(", suffix=")"
).add()
"""Lowercase English alphabet: a, b, c, ..., aa, ab..."""
# https://www.w3.org/TR/predefined-counter-styles/#lower-alpha

UPPER_ALPHA = NumFormat("upper-alpha", to_alpha).add()
"""Uppercase English alphabet: A, B, C, ..., AA, AB..."""
# https://www.w3.org/TR/predefined-counter-styles/#upper-alpha

LOWER_ROMAN = NumFormat(
    "lower-roman", call_lower(to_roman), prefix="(", suffix=")"
).add()
"""Lowercase Roman numerals: i, ii, iii..."""
# https://www.w3.org/TR/predefined-counter-styles/#lower-roman

UPPER_ROMAN = NumFormat("upper-roman", to_roman).add()
"""Uppercase Roman numerals: I, II, III..."""
# https://www.w3.org/TR/predefined-counter-styles/#upper-roman

NUMERIC_ORDINAL = NumFormat("numeric-ordinal", to_nth).add()
"""Numeric ordinals: 1st, 2nd, 3rd..."""

LOWER_ORDINAL = NumFormat("lower-ordinal", to_ordinal).add()
"""Lowercase ordinals: first, second, third..."""

TITLE_ORDINAL = NumFormat("title-ordinal", call_title(to_ordinal)).add()
"""Title-case ordinals: First, Second, Third, Fourth, Fifth..."""

LOWER_CARDINAL = NumFormat("lower-cardinal", to_cardinal).add()
"""Lowercase cardinals: one, two, three, four, five..."""

TITLE_CARDINAL = NumFormat("title-cardinal", call_title(to_cardinal)).add()
"""Title-case cardinals: One, Two, Three, Four, Five..."""

Global variables

var ALPHABET

English alphabet.

var ROMAN_VALS

Roman numeral values.

var ROMAN_SYMS

Roman numeral symbols.

var NAME_ONES : Dict[int, str]

Name of numbers under 20.

var NAME_TENS : Dict[int, str]

Name of the tens.

var NAME_ILLIONS : Dict[int, str]

Names of the larger numbers.

var ORDINAL_SUFFIXES

Special ordinal suffix replacements.

var NUMERAL_FUNC

Render a numeral from an integer.

var NULL_FORMAT : str

Special empty formatting.

var LOWER_ALPHA : str

Lowercase English alphabet: a, b, c, …, aa, ab…

var UPPER_ALPHA : str

Uppercase English alphabet: A, B, C, …, AA, AB…

var LOWER_ROMAN : str

Lowercase Roman numerals: i, ii, iii…

var UPPER_ROMAN : str

Uppercase Roman numerals: I, II, III…

var NUMERIC_ORDINAL : str

Numeric ordinals: 1st, 2nd, 3rd…

var LOWER_ORDINAL : str

Lowercase ordinals: first, second, third…

var TITLE_ORDINAL : str

Title-case ordinals: First, Second, Third, Fourth, Fifth…

var LOWER_CARDINAL : str

Lowercase cardinals: one, two, three, four, five…

var TITLE_CARDINAL : str

Title-case cardinals: One, Two, Three, Four, Five…

Functions

def commafy(num: int) ‑> str

Return a comma-grouped number.

Expand source code
def commafy(num: int) -> str:
    """Return a comma-grouped number."""
    return f"{num:,}"
def to_decimal(num: int) ‑> str

Decimal numerals.

Expand source code
def to_decimal(num: int) -> str:
    """Decimal numerals."""
    # https://en.wikipedia.org/wiki/Decimal
    return str(num)
def to_alpha(num: int) ‑> str

English alphabetical numerals.

Expand source code
def to_alpha(num: int) -> str:
    """English alphabetical numerals."""
    # https://en.wikipedia.org/wiki/English_alphabet
    result = ""
    if num < 1:
        return result

    while num > 0:
        num, remainder = divmod(num - 1, 26)
        result = ALPHABET[remainder] + result

    return result
def to_roman(num: int) ‑> str

Return the Roman numeral.

Expand source code
def to_roman(num: int) -> str:
    """Return the [Roman numeral](https://en.wikipedia.org/wiki/Roman_numerals)."""
    result = ""
    if num < 1:
        return result

    idx = 0
    while num > 0:
        for _ in range(num // ROMAN_VALS[idx]):
            result += ROMAN_SYMS[idx]
            num -= ROMAN_VALS[idx]
        idx += 1
    return result
def to_cardinal(num: int) ‑> str

English cardinal numerals.

Expand source code
def to_cardinal(num: int) -> str:
    """English cardinal numerals."""
    # https://en.wikipedia.org/wiki/Cardinal_numeral
    # See: https://stackoverflow.com/a/54018199
    if num == 0:
        return "zero"
    if num < 0:
        return _space_join("negative", _pos(-num))
    return _pos(num)
def to_nth(num: int) ‑> str

Numeric ordinal numerals ("1st", "2nd").

Expand source code
def to_nth(num: int) -> str:
    """Numeric ordinal numerals ("1st", "2nd")."""
    # https://en.wikipedia.org/wiki/Ordinal_numeral
    if 10 <= num % 100 <= 20:
        suffix = "th"
    else:
        suffixes = {1: "st", 2: "nd", 3: "rd"}
        suffix = suffixes.get(num % 10, "th")
    return str(num) + suffix
def to_ordinal(num: int) ‑> str

English ordinal numerals ("first", "second").

Expand source code
def to_ordinal(num: int) -> str:
    """English ordinal numerals ("first", "second")."""
    # https://en.wikipedia.org/wiki/Ordinal_numeral
    cardinal = to_cardinal(num)
    for check, suffix in ORDINAL_SUFFIXES.items():
        if cardinal.endswith(check):
            return cardinal[: -len(check)] + suffix
    return cardinal + "th"
def call_lower(f: NUMERAL_FUNC) ‑> Callable[[int], str]

Call .lower() after calling a render function.

Expand source code
def call_lower(f: NUMERAL_FUNC) -> NUMERAL_FUNC:
    """Call `.lower()` after calling a render function."""
    return lambda n: f(n).lower()
def call_title(f: NUMERAL_FUNC) ‑> Callable[[int], str]

Call .title() after calling a render function.

Expand source code
def call_title(f: NUMERAL_FUNC) -> NUMERAL_FUNC:
    """Call `.title()` after calling a render function."""
    return lambda n: f(n).title()

Classes

class NumFormat (name: str, numeral: NUMERAL_FUNC, prefix: str = '', suffix: str = '')

Numeral formatter.

Expand source code
class NumFormat(Registrable):
    """Numeral formatter."""

    name: str
    """Numeral system name."""

    numeral: NUMERAL_FUNC
    """Function that converts an integer to a numeral."""

    prefix: str = ""
    """Prefix string when rendering."""

    suffix: str = ""
    """Suffix string when rendering."""

    def render(self, num: int, punctuation: bool = False) -> str:
        """Render the numeral."""
        val = self.numeral(num)
        return val if not punctuation else f"{self.prefix}{val}{self.suffix}"

    __call__ = render

Ancestors

Class variables

var name : str

Inherited from: Registrable.name

Name of this object.

var numeral : Callable[[int], str]

Function that converts an integer to a numeral.

var prefix : str

Prefix string when rendering.

var suffix : str

Suffix string when rendering.

var DEFAULTRegistrable

Inherited from: Registrable.DEFAULT

Default value of this type.

Static methods

def get(name: Union[str, T], default: Optional[T] = None) ‑> ~T

Inherited from: Registrable.get

Get the requested named object.

Methods

def render(self, num: int, punctuation: bool = False) ‑> str

Render the numeral.

Expand source code
def render(self, num: int, punctuation: bool = False) -> str:
    """Render the numeral."""
    val = self.numeral(num)
    return val if not punctuation else f"{self.prefix}{val}{self.suffix}"
def add(self: T, override: bool = False) ‑> ~T

Inherited from: Registrable.add

Add this name to the registry.