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 };