Functional Weave
Code in Rust

retail.price-per-unit@1.0.1

impl/python.py

2,193 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 .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune
from .units_convert import convert_units  ← from units.convert ^1.0.0 · built alongside by fune
from .retail_price_per_unit_types import UnitPriceBasis

# Each basis is a units.convert symbol and how many of it the price is for.
_BASES = {
    "kg": ("kg", 1),
    "100g": ("g", 100),
    "10g": ("g", 10),
    "L": ("L", 1),
    "100mL": ("mL", 100),
    "10mL": ("mL", 10),
    "75cL": ("mL", 750),
    "m": ("m", 1),
    "m2": ("m2", 1),
    "m3": ("m3", 1),
    "item": ("item", 1),
}

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


def unit_price(price: Money, quantity: str, unit: str, per: UnitPriceBasis) -> Money:
    """The price of one ``per`` of the product, rounded half up to the minor unit.

    The quantity is converted exactly (units.convert, 12 decimal places), then
    price x multiplier / quantity is done in integers, so the one rounding step
    is the last one.
    """
    if price.minor < 0:
        raise ValueError("price must not be negative, received %d" % price.minor)
    if per not in _BASES:
        raise ValueError('unknown unit price basis "%s"' % (per,))
    target, multiplier = _BASES[per]

    if target == "item" or unit == "item":
        if target != "item":
            raise ValueError('a quantity in items can only be priced per "item"')
        if unit != "item":
            raise ValueError('per "item" needs the quantity in "item"')
        if not isinstance(quantity, str) or not _DECIMAL.fullmatch(quantity):
            raise ValueError('quantity must be more than 0, received "%s"' % (quantity,))
        converted = quantity
    else:
        converted = convert_units(quantity, unit, target, 12)
    if converted.startswith("-") or not re.search(r"[1-9]", converted):
        raise ValueError('quantity must be more than 0, received "%s"' % (quantity,))

    # converted = digits / 10^places, so price / converted = price x 10^places / digits.
    whole, _, fraction = converted.partition(".")
    digits = int(whole + fraction)
    numerator = price.minor * multiplier * 10 ** len(fraction)
    quotient, remainder = divmod(numerator, digits)
    if remainder * 2 >= digits:
        quotient += 1
    return money(quotient, price.currency)