feat: annotate scaled ingredient quantities

Refs #1, #2
This commit is contained in:
2026-08-14 13:28:39 -05:00
parent de8eab9022
commit b61ca70bed
2 changed files with 52 additions and 15 deletions
+34 -14
View File
@@ -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 };