impl/typescript.ts
2,235 bytes · the TypeScript 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 { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { convertUnits } from "./units_convert.ts"; ← from units.convert ^1.0.0 · built alongside by fune
import { type UnitPriceBasis } from "./retail_price_per_unit_types.ts";
// Each basis is a units.convert symbol and how many of it the price is for.
const BASES: Record<string, [string, number]> = {
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],
};
const DECIMAL = /^[0-9]+(\.[0-9]+)?$/;
/**
* 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.
*/
export function unitPrice(price: Money, quantity: string, unit: string, per: UnitPriceBasis): Money {
if (price.minor < 0) throw new RangeError(`price must not be negative, received ${price.minor}`);
const basis = BASES[per];
if (basis === undefined) throw new RangeError(`unknown unit price basis "${per}"`);
const [target, multiplier] = basis;
let converted: string;
if (target === "item" || unit === "item") {
if (target !== "item") throw new RangeError(`a quantity in items can only be priced per "item"`);
if (unit !== "item") throw new RangeError(`per "item" needs the quantity in "item"`);
if (!DECIMAL.test(quantity)) throw new RangeError(`quantity must be more than 0, received "${quantity}"`);
converted = quantity;
} else {
converted = convertUnits(quantity, unit, target, 12);
}
if (converted.startsWith("-") || !/[1-9]/.test(converted)) {
throw new RangeError(`quantity must be more than 0, received "${quantity}"`);
}
// converted = digits / 10^places, so price / converted = price x 10^places / digits.
const [whole, fraction = ""] = converted.split(".");
const digits = BigInt(whole + fraction);
const numerator = BigInt(price.minor) * BigInt(multiplier) * 10n ** BigInt(fraction.length);
let quotient = numerator / digits;
if ((numerator % digits) * 2n >= digits) quotient += 1n;
return money(Number(quotient), price.currency);
}