63 lines
2.5 KiB
JavaScript
63 lines
2.5 KiB
JavaScript
const FRACTIONS = new Map([
|
|
["¼", 1 / 4], ["½", 1 / 2], ["¾", 3 / 4], ["⅓", 1 / 3],
|
|
["⅔", 2 / 3], ["⅛", 1 / 8], ["⅜", 3 / 8], ["⅝", 5 / 8], ["⅞", 7 / 8]
|
|
]);
|
|
|
|
function parseYield(value) {
|
|
const match = String(value ?? "").match(/\d+(?:\.\d+)?/);
|
|
if (!match) return null;
|
|
const portions = Number(match[0]);
|
|
return Number.isFinite(portions) && portions > 0 ? portions : null;
|
|
}
|
|
|
|
function parseQuantity(value) {
|
|
if (FRACTIONS.has(value)) return FRACTIONS.get(value);
|
|
if (value.includes("/")) {
|
|
const [numerator, denominator] = value.split("/").map(Number);
|
|
return denominator ? numerator / denominator : null;
|
|
}
|
|
return Number(value);
|
|
}
|
|
|
|
function formatQuantity(value) {
|
|
const whole = Math.floor(value + 1e-8);
|
|
const remainder = value - whole;
|
|
const candidates = [[0, ""], [1 / 8, "⅛"], [1 / 4, "¼"], [1 / 3, "⅓"],
|
|
[3 / 8, "⅜"], [1 / 2, "½"], [5 / 8, "⅝"], [2 / 3, "⅔"],
|
|
[3 / 4, "¾"], [7 / 8, "⅞"], [1, ""]];
|
|
const [fraction, symbol] = candidates.reduce((best, candidate) => (
|
|
Math.abs(candidate[0] - remainder) < Math.abs(best[0] - remainder) ? candidate : best
|
|
));
|
|
if (Math.abs(fraction - remainder) <= 0.02) {
|
|
const adjustedWhole = fraction === 1 ? whole + 1 : whole;
|
|
return `${adjustedWhole || ""}${adjustedWhole && symbol ? " " : ""}${symbol}` || "0";
|
|
}
|
|
return Number(value.toFixed(2)).toString();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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*/, "");
|
|
return {
|
|
...recipe,
|
|
recipeYield: suffix ? `${portions} ${suffix}` : portions,
|
|
recipeIngredient: (recipe.recipeIngredient || []).map((ingredient) => (
|
|
scaleIngredient(ingredient, portions / basePortions)
|
|
))
|
|
};
|
|
}
|
|
|
|
module.exports = { parseYield, scaleIngredient, scaleRecipe };
|