Functional Weave
Code in Python

retail.price-per-unit

Unit price for shelf-edge labels: price per kg, per 100 g, per litre, per 100 ml, per metre or per item.

1.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 27 tests, run in TypeScript, Python and Rust.

What it does

The unit price on a shelf-edge label: what one kilogram, one litre, one metre or one item of the product costs, so shoppers can compare packs of different sizes. `unitPrice(£1.50, "500", "g", "kg")` is £3.00.

## Which unit (UK)

For example

  • unit_price(£1.50, 500, g, kg) → £3.00 500 g for 1.50 is 3.00 per kg
  • unit_price(£1.50, 500, g, 100g) → £0.30 500 g for 1.50 is 30p per 100 g
  • unit_price(£2.00, 1.5, L, L) → £1.33 1.5 L for 2.00 is 1.33 per litre

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

def unit_price(price: Money, quantity: str, unit: str, per: UnitPriceBasis) -> Money
priceMoneyselling price of the pack, VAT included, 0 or more
quantitystringpack quantity as decimal text: "500", "1.5", "12"
unitstringa units.convert symbol ("g", "kg", "mL", "L", "oz", "m", "m2") or "item"
perUnitPriceBasisthe unit the price is quoted per
returnsMoneyprice for one of `per`, rounded half up to the minor unit

The type it declares, generated into your project

UnitPriceBasis = Literal["kg", "100g", "10g", "L", "100mL", "10mL", "75cL", "m", "m2", "m3", "item"]

Your code names it in one line, in the file that uses it

from fune.retail.price_per_unit import unit_price  # retail.price-per-unit@^1
impl/python.py · 58 lines · open · 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)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add retail.price-per-unit
Download for Python retail.price-per-unit-1.0.1-python.fune · 12,905 bytes sha256 4ab9f40bfde3bc1dda020b8fe4fa5870e136427ec377a6d61687d9a96c2e1860

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./retail.price-per-unit-1.0.1-python.fune, or fetch it from a terminal with fune pull retail.price-per-unit@1.0.1:python.

The whole function, every language, is one file too: retail.price-per-unit-1.0.1.fune, 18,495 bytes, sha256 bb9baca1fabbefbfb0d311fb8f46242ae294c3a78f05a541398b361ce3e780e5. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

# fune: before retail.price-per-unit

after — your function gets the result and the arguments, and returns the final result.

# fune: after retail.price-per-unit

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

# fune: replace money.amount in retail.price-per-unit
# fune: replace units.convert in retail.price-per-unit

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show retail.price-per-unit --steps.

# fune: step retail.price-per-unit after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
500 g for 1.50 is 3.00 per kg £1.50, 500, g, kg → £3.00
500 g for 1.50 is 30p per 100 g £1.50, 500, g, 100g → £0.30
1.5 L for 2.00 is 1.33 per litre £2.00, 1.5, L, L → £1.33
1.5 L for 2.00 is 13p per 100 ml £2.00, 1.5, L, 100mL → £0.13
a 330 ml can for 85p is 2.58 per litre £0.85, 330, mL, L → £2.58
454 g for 3.49 is 7.69 per kg £3.49, 454, g, kg → £7.69
one pound for 4.00 is 8.82 per kg, exactly, where a float division can drift £4.00, 1, lb, kg → £8.82
8 oz for 2.50 is 1.10 per 100 g (8 oz = 226.796185 g exactly) £2.50, 8, oz, 100g → £1.10
a 38 g jar of spice for 1.20 is 32p per 10 g £1.20, 38, g, 10g → £0.32
a 750 ml bottle of wine for 9.00 is 9.00 per 75 cl £9.00, 750, mL, 75cL → £9.00
Show the other 17 tests
CaseArgumentsExpected
a magnum for 6.00 is 3.00 per 75 cl £6.00, 1.5, L, 75cL → £3.00
a dozen eggs for 3.00 is 25p each £3.00, 12, item, item → £0.25
6 rolls for 1.00 is 16.67p, shown as 17p £1.00, 6, item, item → £0.17
an exact half penny rounds up, not to even £0.25, 2, item, item → £0.13
10 m of cable for 7.99 is 80p per metre £7.99, 10, m, m → £0.80
1.2 square metres of flooring for 30.00 is 25.00 per m2 £30.00, 1.2, m2, m2 → £25.00
a quantity with a trailing zero £1.50, 0.50, kg, kg → £3.00
a free item has a unit price of 0 £0.00, 400, g, kg → £0.00
euro cents work the same €1.99, 250, g, kg → €7.96
a zero quantity is an error £1.50, 0, g, kg → error: quantity must be more than 0
a negative quantity is an error £1.50, -1, kg, kg → error: quantity must be more than 0
mass cannot be priced per litre £1.50, 500, g, L → error: cannot convert g (mass) to L (volume)
items cannot be priced per kilogram £3.00, 12, item, kg → error: a quantity in items can only be priced per "item"
grams cannot be priced per item £3.00, 500, g, item → error: per "item" needs the quantity in "item"
a malformed quantity is an error £1.50, 1,5, kg, kg → error: value must be a plain decimal
a negative price is an error -£1.50, 500, g, kg → error: price must not be negative
a trailing newline is not part of an item quantity £3.00, 12 , item, item → error: quantity must be more than 0

More from the author

The Price Marking Order 2004 (SI 2004/102) requires a unit price alongside the selling price for most goods sold loose or in pre-set quantities, and defines it as the final price, VAT included, per kilogram, litre, metre, square metre or cubic metre, or per item for goods sold by number.

- **Until 5 April 2026**, Schedule 1 of the Order let listed products use other units: per 10 g or 10 ml (herbs, spices, make-up), per 100 g or 100 ml (biscuits, coffee, soft drinks, sauces and many more), per 75 cl for wine, per 50 kg for coal. - **From 6 April 2026**, the Price Marking (Amendment) Order 2024 (SI 2024/1055, commencement moved from 1 October 2025 by SI 2025/592) omits Schedule 1. The unit price is per item for goods sold by number, per kilogram for goods marked only by weight, per litre for goods marked only by volume, and otherwise per kilogram, litre, metre, square metre or cubic metre. The deposit (for example a deposit return scheme charge) is excluded.

This function does not decide which unit a product needs; that depends on the product and on the date, and the caller's product data knows both. It takes the unit as `per` and does the arithmetic exactly. `100g`, `10g`, `100mL`, `10mL` and `75cL` stay available for labels printed under the old Schedule 1, for shelf labels outside Great Britain, and for voluntary extra prices.

## Arithmetic

The pack quantity is converted to the unit of `per` with `units.convert`, which is exact rational arithmetic, to 12 decimal places (so 8 oz is exactly 226.796185 g). The price is then divided by that quantity in exact integer arithmetic and rounded once, half up, to the minor unit: 6 rolls for £1.00 is 16.67p, shown as 17p. There is no floating point anywhere, so £4.00 per pound is 882p per kg in every language.

## Edge cases

- `unit` `"item"` goes with `per` `"item"` only, and the other way round. - A quantity of zero, or one so small it converts to 0 at 12 decimal places, is an error; so is a negative price. A free item's unit price is 0. - Mass and volume do not convert into each other: pricing 500 g per litre is an error from `units.convert`. Where the law lets a product be sold by weight or volume, pass whichever the pack is marked with.

## Sources

- The Price Marking Order 2004, SI 2004/102, article 1 (definition of "unit price") and Schedule 1, as made: https://www.legislation.gov.uk/uksi/2004/102/article/1/made and https://www.legislation.gov.uk/uksi/2004/102/schedule/1/made - The Price Marking (Amendment) Order 2024, SI 2024/1055 (new definition of "unit price", Schedule 1 omitted): https://www.legislation.gov.uk/uksi/2024/1055/made - The Price Marking (Amendment) Order 2025, SI 2025/592, article 2(2) (commencement moved to 6 April 2026): https://www.legislation.gov.uk/uksi/2025/592/body/made

1.0.1 fixes Python accepting a trailing newline in quantity (priced per item); adds tests.

Files

PathBytes
README.md3,185
impl/python.py2,193
impl/rust.rs3,027
impl/typescript.ts2,235
vectors.json4,450