A recipe-cost calculator is a useful small example of a broader programming problem: the unit you buy is not always the unit you can use.
Suppose a hypothetical ingredient costs $10 for 1,000 g, but trimming leaves 80% usable weight. If a recipe needs 100 g of the usable ingredient, its cost is $1.25, rather than $1.00.
The interesting engineering work is keeping those meanings clear at the input boundary, then preserving them through validation, calculation and display.
Write down the quantity basis first
For this example:
-
packGramsis the purchased weight, before the loss represented by yield. -
yieldPercentconverts that purchased weight into usable weight. -
usedGramsis the usable quantity needed by the recipe. -
packPriceis the price for the entire purchased pack, in one chosen currency.
The formula follows directly:
usable pack weight = purchased pack weight × yield / 100
ingredient cost = pack price × usable recipe quantity / usable pack weight
The denominator matters. Dividing by 0.8 increases the cost per usable gram; multiplying the final cost by 0.8 would make the loss look like a discount.
This model is only correct when the yield and the recipe quantity describe the same preparation stage. A raw trimmed weight and a cooked weight are different bases. Do not apply a cooking yield again to an ingredient that was already entered on a cooked basis.
Keep a small, pure calculation function
This standalone teaching example accepts numeric values in grams. It is not a copy of a complete application's calculation engine.
function ingredientCost({ packPrice, packGrams, yieldPercent, usedGrams }) {
for (const [name, value] of Object.entries({
packPrice, packGrams, yieldPercent, usedGrams
})) {
if (!Number.isFinite(value)) {
throw new TypeError(`${name} must be a finite number`);
}
}
if (packPrice < 0 || packGrams <= 0 || usedGrams < 0 ||
yieldPercent <= 0 || yieldPercent > 100) {
throw new RangeError('Invalid price, mass, or yield');
}
const edibleGrams = packGrams * yieldPercent / 100;
return packPrice * usedGrams / edibleGrams;
}
const cost = ingredientCost({
packPrice: 10,
packGrams: 1000,
yieldPercent: 80,
usedGrams: 100
});
console.log(cost); // 1.25
Zero usage and a zero price are meaningful inputs, so they remain valid. A zero pack weight or zero yield would make the denominator invalid. Negative quantities, non-finite values and numeric-looking strings are rejected by the calculation function.
Keep DOM parsing outside this function. An empty input should stay a validation error rather than silently becoming a zero through Number(''). Parse a non-empty field, check its range, and display a field-specific message before calling the calculation.
Normalize units before calculating
The example deliberately does not accept arbitrary unit strings. Convert compatible mass units into grams at a separate boundary. Keep the original unit and value for display and export.
Volume is another dimension. Converting a cup of an ingredient into grams requires a relevant density or measured mass. A generic conversion table cannot safely treat every cup as the same number of grams. Reject an unsupported mass/volume combination rather than returning a plausible-looking answer.
Check behavior, not only one output
These checks capture the meaning of the inputs:
| Change to the example | Expected result |
|---|---|
| Yield becomes 100% | $1.00 |
| Usable recipe quantity doubles | $2.50 |
| Usable recipe quantity becomes zero | $0.00 |
| Pack price becomes zero | $0.00 |
| Yield becomes 0% or 101% | Reject |
| Pack weight becomes zero | Reject |
Input is NaN, Infinity, a string or a boolean |
Reject |
Before publication, the sample function was executed against 14 assertions covering those cases and negative-input boundaries. That checks the teaching function; it does not certify a full product, an accounting system or a real kitchen's data.
Round for display, not at every intermediate step
Keep the unrounded ingredient values when calculating a batch total and portion cost. Formatting every intermediate value to two decimals can accumulate rounding differences across many ingredients.
JavaScript Number is binary floating point. For an estimate displayed in a browser, a formatter can present the result in the selected currency. A payment ledger or another workflow requiring exact settlement needs its own decimal and rounding rules; changing the display formatter does not create accounting precision.
Ingredient cost also is not profit. Labor, overhead, waste assumptions, taxes and payment fees belong in separately defined inputs and outputs. Calling a number “profit” when it only subtracts ingredients is a labeling bug even when the subtraction is correct.
A browser worksheet to explore
Kevanto provides a free recipe-cost worksheet, including yield inputs, batch scaling, local recipe storage and CSV/JSON exports. The blank and worked costing worksheet can also be used to follow a calculation on paper. This account is associated with the project; these links are a product disclosure, not an independent review.
AI disclosure: This article and its illustrative code were generated by an AI assistant for the product owner. The sample checks were executed before publication. No firsthand restaurant operating experience, customer outcomes or measured savings are claimed.
Top comments (0)