Functional Weave
Code in Rust

agri.yield-per-hectare@1.0.1

impl/python.py

3,333 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import re
from typing import List, Sequence, Tuple

from .agri_yield_per_hectare_types import YieldPerArea
from .units_convert import convert_units  ← from units.convert ^1.0.0 · built alongside by fune

DECIMAL = re.compile(r"[0-9]+(\.[0-9]+)?")

Decimal = Tuple[int, int]  # digits, scale: digits / 10^scale


def _parse(name: str, text: str, limited: bool) -> Decimal:
    if not isinstance(text, str) or not DECIMAL.fullmatch(text):
        raise ValueError('%s must be plain non-negative decimal text like "12.5", received "%s"' % (name, text))
    whole, _, raw_fraction = text.partition(".")
    fraction = raw_fraction.rstrip("0")
    significant = (whole + fraction).lstrip("0")
    if limited and (len(significant) > 12 or len(fraction) > 6):
        raise ValueError('%s "%s" has too many digits: at most 12 significant digits and 6 decimal places' % (name, text))
    return (int(significant) if significant else 0, len(fraction))


def _ratio(top: Sequence[Decimal], bottom: Sequence[Decimal]) -> Tuple[int, int]:
    """(product of top) / (product of bottom), each a decimal, as integers."""
    num, den, exponent = 1, 1, 0
    for digits, scale in top:
        num *= digits
        exponent -= scale
    for digits, scale in bottom:
        den *= digits
        exponent += scale
    if exponent > 0:
        num *= 10**exponent
    else:
        den *= 10 ** (-exponent)
    return num, den


def _to_text(fraction_pair: Tuple[int, int], decimals: int) -> str:
    """Long division, one digit at a time, rounded half up, trailing zeros trimmed."""
    num, den = fraction_pair
    whole, remainder = divmod(num, den)
    fraction = 0
    for _ in range(decimals):
        remainder *= 10
        fraction = fraction * 10 + remainder // den
        remainder %= den
    if remainder * 2 >= den:
        fraction += 1
        if fraction == 10**decimals:
            fraction = 0
            whole += 1
    text = str(whole)
    if decimals > 0:
        shown = str(fraction).rjust(decimals, "0").rstrip("0")
        if shown:
            text += "." + shown
    return text


def yield_per_hectare(quantity: str, area: str, area_unit: str, decimals: int) -> YieldPerArea:
    """Yield per hectare and per acre, exactly.

    The area unit's size comes from units.convert as exact decimal text
    (1 acre = 4046.8564224 m2), so every answer is one exact fraction rounded
    once. In floats, 1.005 t from 1 ha is 1.00 t/ha to two places; here it is
    1.01.
    """
    q = _parse("quantity", quantity, True)
    a = _parse("area", area, True)
    if isinstance(decimals, bool) or not isinstance(decimals, int) or decimals < 0 or decimals > 12:
        raise ValueError("decimals must be a whole number from 0 to 12, received %r" % (decimals,))
    unit = _parse("unit size", convert_units("1", area_unit, "m2", 12), False)
    if a[0] == 0:
        raise ValueError("area must be greater than zero")
    acre = _parse("acre", convert_units("1", "acre", "m2", 12), False)
    hectare: Decimal = (10000, 0)
    parts: List[str] = [
        _to_text(_ratio([q, hectare], [a, unit]), decimals),
        _to_text(_ratio([q, acre], [a, unit]), decimals),
        _to_text(_ratio([a, unit], [hectare]), decimals),
        _to_text(_ratio([a, unit], [acre]), decimals),
    ]
    return YieldPerArea(per_hectare=parts[0], per_acre=parts[1], hectares=parts[2], acres=parts[3])