Files
MMM-NextcloudCookbook/lib/recipe-scaler.js
T

83 lines
3.2 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 {
value: `${adjustedWhole || ""}${adjustedWhole && symbol ? " " : ""}${symbol}` || "0",
approximate: false
};
}
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) {
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,
originalRecipeYield: recipe.recipeYield,
recipeIngredient: ingredients.map(({ value }) => value),
recipeIngredientStatus: ingredients.map(({ status }) => status)
};
}
module.exports = { parseYield, scaleIngredient, scaleIngredientWithStatus, scaleRecipe };