import re from typing import List, Sequence, Tuple from .agri_yield_per_hectare_types import YieldPerArea from .units_convert import convert_units 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])