recipe-scale-conv
v0.1.1
Published
Ingredient measurement conversion, formatting, and recipe scaling utilities.
Readme
recipe-scale-conv
Recipe scaling and measurement formatting for apps that need results cooks would actually use.
It handles fractional quantities, US/metric display, ingredient-specific cup-to-gram conversions, count ingredients like eggs, and configurable unit promotion/demotion.
Install
npm install recipe-scale-convThis package supports ESM and CommonJS, supports Node.js 18 and newer, and has no runtime dependencies.
Quick Start
import {
createRecipeConverter,
formatIngredientMeasurement,
parseIngredientLine,
scaleIngredientComponentsForDisplay,
scaleIngredientForDisplay,
scaleIngredientsForDisplay,
scaleRecipeYield
} from 'recipe-scale-conv';CommonJS works too:
const { formatIngredientMeasurement } = require('recipe-scale-conv');formatIngredientMeasurement({ name: 'All-purpose flour', quantity: '1', unit: 'cup' }, 'metric');
// { quantity: '125', unit: 'g' }
scaleIngredientForDisplay({ name: 'vanilla', quantity: '1', unit: 'tbsp' }, 0.5, 'us');
// { name: 'vanilla', quantity: '1 ½', unit: 'tsp' }The library is dependency-free at runtime and treats kitchen conversions as approximate.
What It Does
- Parses common ingredient lines into structured data.
- Scales quantities up or down with numbers, decimals, fractions, and mixed fractions.
- Promotes or demotes units to practical cooking amounts, such as
4 tbspto¼ cup. - Converts between US and metric display with configurable metric rules.
- Uses ingredient-aware cup-to-gram conversions for common baking ingredients.
- Handles count ingredients such as eggs, cloves, pieces, and slices.
- Lets apps set conversion and formatting defaults once with
createRecipeConverter.
Common Workflows
Parse Ingredient Lines
parseIngredientLine('1 1/2 cups all-purpose flour, sifted');
// {
// original: '1 1/2 cups all-purpose flour, sifted',
// quantity: '1 1/2',
// unit: 'cup',
// name: 'all-purpose flour',
// note: 'sifted'
// }The parser also handles adjacent unicode mixed fractions, count units, and common package-size prefixes:
parseIngredientLine('1½ cups all-purpose flour');
// {
// original: '1½ cups all-purpose flour',
// quantity: '1½',
// unit: 'cup',
// name: 'all-purpose flour',
// note: undefined
// }
parseIngredientLine('3 cloves garlic');
// {
// original: '3 cloves garlic',
// quantity: '3',
// unit: 'cloves',
// name: 'garlic',
// note: undefined
// }
parseIngredientLine('1 (14-ounce) can tomatoes, drained');
// {
// original: '1 (14-ounce) can tomatoes, drained',
// quantity: '1',
// unit: 'can',
// name: 'tomatoes',
// note: '14-ounce; drained'
// }Scale One Ingredient
scaleIngredientForDisplay({ name: 'flour', quantity: '1', unit: 'cup' }, 0.5, 'us');
// { name: 'flour', quantity: '½', unit: 'cup' }
scaleIngredientForDisplay({ name: 'flour', quantity: '2', unit: 'tbsp' }, 2, 'us');
// { name: 'flour', quantity: '¼', unit: 'cup' }Scaling factors can be numbers or quantity-like strings:
scaleIngredientForDisplay({ name: 'flour', quantity: '2', unit: 'cup' }, '0.75', 'us');
// { name: 'flour', quantity: '1 ½', unit: 'cup' }A scale factor of 1 still formats the ingredient for the requested target system:
scaleIngredientForDisplay({ name: 'All-purpose flour', quantity: '1', unit: 'cup' }, 1, 'metric');
// { name: 'All-purpose flour', quantity: '125', unit: 'g' }Scale A Whole Ingredient List
const ingredients = [
{ name: 'All-purpose flour', quantity: '1', unit: 'cup' },
{ name: 'eggs', quantity: '3', unit: '' }
];
scaleIngredientsForDisplay(ingredients, 0.5, 'metric');
// [
// { name: 'All-purpose flour', quantity: '63', unit: 'g' },
// { name: 'eggs', quantity: '1 ½', unit: '' }
// ]Scale Recipe Yield
scaleRecipeYield('8 servings', 0.5);
// '4 servings'
scaleRecipeYield('Makes 2 loaves', 1.5);
// 'Makes 3 loaves'
scaleRecipeYield('Serves 4-6', 2);
// 'Serves 8-12'Yield labels round to whole numbers by default. Use yieldScaling: 'fractional', floor, or ceil when your app needs a different policy.
scaleRecipeYield('5 servings', 0.5);
// '3 servings'
scaleRecipeYield('5 servings', 0.5, { yieldScaling: 'fractional' });
// '2 ½ servings'Convert For Metric Display
formatIngredientMeasurement({ name: 'All-purpose flour', quantity: '1', unit: 'cup' }, 'metric');
// { quantity: '125', unit: 'g' }
formatIngredientMeasurement({ name: 'Water', quantity: '1', unit: 'cup' }, 'metric', { metricMode: 'volume' });
// { quantity: '237', unit: 'ml' }By default, metric display converts cups and mass units, while preserving small spoon measures such as tsp and tbsp.
formatIngredientMeasurement({ name: 'vanilla', quantity: '2', unit: 'tbsp' }, 'metric');
// { quantity: '2', unit: 'tbsp' }Configuration
Set App Defaults Once
Use createRecipeConverter when your app has house rules. Per-call options still override these defaults.
const converter = createRecipeConverter({
defaults: {
countScaling: 'round',
unitScaling: 'preserve',
metricConversion: [{ from: ['tsp', 'tbsp'], convert: false }],
fractions: 'ascii'
},
densities: [{ aliases: ['almond flour'], gramsPerCup: 96 }]
});
converter.scaleIngredientForDisplay({ name: 'eggs', quantity: '3', unit: '' }, 0.5, 'us');
// { name: 'eggs', quantity: '2', unit: '' }
converter.formatIngredientMeasurement({ name: 'Almond flour', quantity: '1', unit: 'cup' }, 'metric');
// { quantity: '96', unit: 'g' }Unit Promotion And Demotion
unitScaling controls what happens after scaling measured units.
scaleIngredientForDisplay({ name: 'flour', quantity: '2', unit: 'tbsp' }, 2, 'us', { unitScaling: 'auto' });
// { name: 'flour', quantity: '¼', unit: 'cup' }
scaleIngredientForDisplay({ name: 'flour', quantity: '2', unit: 'tbsp' }, 2, 'us', { unitScaling: 'preserve' });
// { name: 'flour', quantity: '4', unit: 'tbsp' }
scaleIngredientForDisplay({ name: 'flour', quantity: '1', unit: 'cup' }, 0.125, 'us', {
unitScaling: [{ from: 'cup', to: ['tsp'] }]
});
// { name: 'flour', quantity: '6', unit: 'tsp' }auto is the default. It uses common cooking preferences for tsp, tbsp, cup, oz, and lb.
Measurement conversion recognizes teaspoons, tablespoons, cups, fluid ounces, milliliters, liters, ounces, pounds, grams, and kilograms, including common singular, plural, and abbreviated spellings.
Metric Conversion Rules
metricConversion controls which source units convert when formatting for metric display.
formatIngredientMeasurement({ name: 'vanilla', quantity: '2', unit: 'tbsp' }, 'metric', {
metricConversion: [{ from: ['tsp', 'tbsp'], convert: false }]
});
// { quantity: '2', unit: 'tbsp' }
formatIngredientMeasurement({ name: 'vanilla', quantity: '2', unit: 'tbsp' }, 'metric', {
metricConversion: [{ from: 'tbsp', convert: true, metricMode: 'volume' }]
});
// { quantity: '30', unit: 'ml' }Supported values:
auto: convert cups and mass units; preserve small spoon measures.all: convert every known unit.preserve: keep every source unit.- rule list: override specific units and let unmatched units follow
auto.
Count Ingredients
Count ingredients are unitless or use count-like units such as egg, clove, piece, or slice.
scaleIngredientForDisplay({ name: 'eggs', quantity: '3', unit: '' }, 0.5, 'us', { countScaling: 'round' });
// { name: 'eggs', quantity: '2', unit: '' }Supported countScaling values:
fractional: preserve the exact scaled amount. This is the default.round: round to the nearest whole count.floor: round down.ceil: round up.component: special handling for egg components.
Recognized count units include egg, clove, piece, slice, can, package, packet, jar, bunch, sprig, each, and their common plural forms.
Egg Components
For fractional eggs, the library can keep the amount as beaten egg or split quarter-through-three-quarter egg remainders into yolk/white rows. Remainders below one-quarter round down, and remainders above three-quarters round up.
scaleIngredientForDisplay({ name: 'eggs', quantity: '3', unit: '' }, 0.5, 'us', {
countScaling: 'component',
eggComponent: 'beaten'
});
// { name: 'eggs', quantity: '1 ½', unit: '', note: 'beaten' }
scaleIngredientComponentsForDisplay({ name: 'eggs', quantity: '3', unit: '' }, 0.5, 'us', {
countScaling: 'component',
eggComponent: 'yolk'
});
// [
// { name: 'egg', quantity: '1', unit: '' },
// { name: 'egg yolk', quantity: '1', unit: '' }
// ]scaleIngredientsForDisplay also uses component splitting, so whole recipe lists can expand one egg row into multiple display rows.
Formatting
formatIngredientMeasurement({ name: 'Milk', quantity: '1/2', unit: 'cup' }, 'us', {
fractions: 'ascii',
unitStyle: 'long'
});
// { quantity: '1/2', unit: 'cup' }
formatIngredientText({ name: 'flour', quantity: '2', unit: 'cup' }, 'us');
// '2 cups flour'Fraction display can be unicode, ascii, or decimal.
Conversion Notes
Kitchen conversions are approximate. US volume conversions use US customary measures, and ingredient-specific cup-to-gram conversions use built-in density assumptions for common ingredients such as butter, flour, sugar, oats, rice, honey, water, and milk.
When converting cups to metric weight, matching ingredient-specific densities take priority. If no matching density exists and metric weight is requested, the library falls back to a water-like approximation where 1 ml is treated as roughly 1 g. Use createRecipeConverter({ densities }) to provide app-specific values for ingredients you care about.
Browser Demo
From a repository checkout, run the interactive example page to try the parser, scaling slider, metric conversion options, fraction formatting, count handling, unit promotion, and custom density overrides in a browser.
npm run demo:webThen open http://127.0.0.1:5173/.
API Reference
Main Functions
| Function | Use it for |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| parseIngredientLine(line) | Parse a common recipe line into { quantity, unit, name, note }. |
| scaleIngredientQuantity(quantity, factor, options?) | Scale a standalone quantity string. |
| scaleIngredientForDisplay(ingredient, factor, targetSystem, options?) | Scale one ingredient object and format it for display. |
| scaleIngredientComponentsForDisplay(ingredient, factor, targetSystem, options?) | Scale one ingredient and allow egg component splits. |
| scaleIngredientsForDisplay(ingredients, factor, targetSystem, options?) | Scale a full ingredient list. |
| scaleRecipeYield(yieldText, factor, options?) | Scale labels such as 8 servings, Makes 2 loaves, or ranges. |
| formatIngredientMeasurement(ingredient, targetSystem, options?) | Format one measurement for us or metric. |
| convertMeasurement(ingredient, targetSystem, options?) | Format one measurement and include conversion metadata. |
| formatIngredientText(ingredient, targetSystem, options?) | Produce display text such as 2 cups flour. |
| createRecipeConverter(options) | Create a converter with app-wide defaults and custom density rules. |
Quantity Helpers
| Function | Use it for |
| ---------------------------------------- | ------------------------------------------------------------------------------------- |
| parseRecipeQuantity(quantity) | Parse decimals, fractions, mixed fractions, glyph fractions, and approximate numbers. |
| formatRecipeQuantity(value, options?) | Format numbers as decimals or fractions. |
| normalizeMeasurementUnit(unit, style?) | Normalize aliases such as Tablespoons to tbsp or tablespoon. |
Types
type MeasurementSystem = 'us' | 'metric';
type ScaleFactor = number | string;
type FractionFormat = 'unicode' | 'ascii' | 'decimal';
type UnitStyle = 'short' | 'long';
type MetricMode = 'auto' | 'weight' | 'volume';
type CountScaling = 'fractional' | 'round' | 'floor' | 'ceil' | 'component';
type YieldScaling = 'round' | 'fractional' | 'floor' | 'ceil';
type EggComponent = 'beaten' | 'yolk' | 'white';
type UnitScaling = 'auto' | 'preserve' | UnitScalingRule[];
type MetricConversion = 'auto' | 'all' | 'preserve' | MetricConversionRule[];type IngredientMeasurement = {
name: string;
quantity?: string;
unit?: string;
note?: string;
};Development
npm install
npm run format
npm run lint
npm run format:check
npm run test
npm run check
npm run build
npm run package:test
npm run pack:dry
npm run release:checknpm run release:check runs the full local release gate. npm run pack:dry verifies the publish contents without creating a tarball, and npm pack runs lint, format checking, type checking, tests, and build through the prepack script. CI runs the same verification on Node.js 18, 20, and 22.
| Command | Use it for |
| ----------------------- | ----------------------------------------------- |
| npm run demo:web | Build the library and serve the browser demo. |
| npm run format | Rewrite files with Prettier. |
| npm run lint | Run ESLint. |
| npm run format:check | Check Prettier formatting without rewriting. |
| npm run test | Run the Vitest suite. |
| npm run check | Type-check TypeScript without emitting output. |
| npm run build | Build ESM, CJS, and declaration output. |
| npm run package:test | Pack and test install/import/type consumption. |
| npm run pack:dry | Verify publish contents without creating a tgz. |
| npm run release:check | Run the full local release gate. |
