+34
-14
@@ -30,33 +30,53 @@ function formatQuantity(value) {
|
||||
));
|
||||
if (Math.abs(fraction - remainder) <= 0.02) {
|
||||
const adjustedWhole = fraction === 1 ? whole + 1 : whole;
|
||||
return `${adjustedWhole || ""}${adjustedWhole && symbol ? " " : ""}${symbol}` || "0";
|
||||
return {
|
||||
value: `${adjustedWhole || ""}${adjustedWhole && symbol ? " " : ""}${symbol}` || "0",
|
||||
approximate: false
|
||||
};
|
||||
}
|
||||
return Number(value.toFixed(2)).toString();
|
||||
return { value: Number(value.toFixed(2)).toString(), approximate: true };
|
||||
}
|
||||
|
||||
function scaleIngredientWithStatus(ingredient, factor) {
|
||||
if (typeof ingredient !== "string" || factor === 1) {
|
||||
return { value: ingredient, status: "unchanged" };
|
||||
}
|
||||
const match = ingredient.match(/^(\s*)(?:(\d+)\s+)?(\d+\/\d+|\d+(?:\.\d+)?|[¼½¾⅓⅔⅛⅜⅝⅞])(?=\s|[a-zA-Z])/);
|
||||
if (!match) {
|
||||
const hasLeadingQuantity = /^\s*(?:\d|[¼½¾⅓⅔⅛⅜⅝⅞])/.test(ingredient);
|
||||
return { value: ingredient, status: hasLeadingQuantity ? "unscalable" : "unquantified" };
|
||||
}
|
||||
const whole = Number(match[2] || 0);
|
||||
const quantity = parseQuantity(match[3]);
|
||||
if (!Number.isFinite(quantity)) return { value: ingredient, status: "unscalable" };
|
||||
const formatted = formatQuantity((whole + quantity) * factor);
|
||||
const replacement = `${match[1]}${formatted.value}`;
|
||||
return {
|
||||
value: replacement + ingredient.slice(match[0].length),
|
||||
status: formatted.approximate ? "approximate" : "scaled"
|
||||
};
|
||||
}
|
||||
|
||||
function scaleIngredient(ingredient, factor) {
|
||||
if (typeof ingredient !== "string" || factor === 1) return ingredient;
|
||||
const match = ingredient.match(/^(\s*)(?:(\d+)\s+)?(\d+\/\d+|\d+(?:\.\d+)?|[¼½¾⅓⅔⅛⅜⅝⅞])(?=\s|[a-zA-Z])/);
|
||||
if (!match) return ingredient;
|
||||
const whole = Number(match[2] || 0);
|
||||
const quantity = parseQuantity(match[3]);
|
||||
if (!Number.isFinite(quantity)) return ingredient;
|
||||
const replacement = `${match[1]}${formatQuantity((whole + quantity) * factor)}`;
|
||||
return replacement + ingredient.slice(match[0].length);
|
||||
return scaleIngredientWithStatus(ingredient, factor).value;
|
||||
}
|
||||
|
||||
function scaleRecipe(recipe, portions) {
|
||||
const basePortions = parseYield(recipe?.recipeYield);
|
||||
if (!basePortions || !Number.isInteger(portions) || portions < 1) return recipe;
|
||||
const suffix = String(recipe.recipeYield).replace(/^\s*\d+(?:\.\d+)?\s*/, "");
|
||||
const factor = portions / basePortions;
|
||||
const ingredients = (recipe.recipeIngredient || []).map((ingredient) => (
|
||||
scaleIngredientWithStatus(ingredient, factor)
|
||||
));
|
||||
return {
|
||||
...recipe,
|
||||
recipeYield: suffix ? `${portions} ${suffix}` : portions,
|
||||
recipeIngredient: (recipe.recipeIngredient || []).map((ingredient) => (
|
||||
scaleIngredient(ingredient, portions / basePortions)
|
||||
))
|
||||
originalRecipeYield: recipe.recipeYield,
|
||||
recipeIngredient: ingredients.map(({ value }) => value),
|
||||
recipeIngredientStatus: ingredients.map(({ status }) => status)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { parseYield, scaleIngredient, scaleRecipe };
|
||||
module.exports = { parseYield, scaleIngredient, scaleIngredientWithStatus, scaleRecipe };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { parseYield, scaleIngredient, scaleRecipe } = require("../lib/recipe-scaler");
|
||||
const { parseYield, scaleIngredient, scaleIngredientWithStatus, scaleRecipe } = require("../lib/recipe-scaler");
|
||||
|
||||
test("extracts the number of portions from a recipe yield", () => {
|
||||
assert.equal(parseYield("6 servings"), 6);
|
||||
@@ -25,4 +25,21 @@ test("returns a scaled copy of a recipe", () => {
|
||||
assert.equal(scaled.recipeYield, "9 servings");
|
||||
assert.deepEqual(scaled.recipeIngredient, ["3 cups oats", "Salt to taste"]);
|
||||
assert.equal(original.recipeYield, "6 servings");
|
||||
assert.equal(scaled.originalRecipeYield, "6 servings");
|
||||
assert.deepEqual(scaled.recipeIngredientStatus, ["scaled", "unquantified"]);
|
||||
});
|
||||
|
||||
test("reports ingredient scaling confidence", () => {
|
||||
assert.deepEqual(scaleIngredientWithStatus("2 cups oats", 1.5), {
|
||||
value: "3 cups oats", status: "scaled"
|
||||
});
|
||||
assert.deepEqual(scaleIngredientWithStatus("Salt to taste", 2), {
|
||||
value: "Salt to taste", status: "unquantified"
|
||||
});
|
||||
assert.deepEqual(scaleIngredientWithStatus("1-2 tbsp oil", 2), {
|
||||
value: "1-2 tbsp oil", status: "unscalable"
|
||||
});
|
||||
assert.deepEqual(scaleIngredientWithStatus("1 cup milk", 0.7), {
|
||||
value: "0.7 cup milk", status: "approximate"
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user