import re from .money_amount import Money, money from .units_convert import convert_units 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.match(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)