import { EMPTY_NUTRITION, type NutritionDeclaration, type NutritionValues, type Unit, } from "@app/shared-types"; import { toBase, toGrams, type IngredientUnitInfo } from "./units.js"; /** Skala näringsvärden med en faktor. */ export function scaleNutrition(values: NutritionValues, factor: number): NutritionValues { const scaled: NutritionValues = { kcal: values.kcal * factor, proteinG: values.proteinG * factor, carbsG: values.carbsG * factor, fatG: values.fatG * factor, saturatedFatG: values.saturatedFatG * factor, fiberG: values.fiberG * factor, sugarG: values.sugarG * factor, saltG: values.saltG * factor, }; if (values.micro) { scaled.micro = Object.fromEntries( Object.entries(values.micro).map(([k, v]) => [k, v == null ? v : v * factor]), ); } return scaled; } /** Summera två näringsvärden. */ export function addNutrition(a: NutritionValues, b: NutritionValues): NutritionValues { const sum: NutritionValues = { kcal: a.kcal + b.kcal, proteinG: a.proteinG + b.proteinG, carbsG: a.carbsG + b.carbsG, fatG: a.fatG + b.fatG, saturatedFatG: a.saturatedFatG + b.saturatedFatG, fiberG: a.fiberG + b.fiberG, sugarG: a.sugarG + b.sugarG, saltG: a.saltG + b.saltG, }; const micros = { ...(a.micro ?? {}) } as Record; if (b.micro) { for (const [k, v] of Object.entries(b.micro)) { if (v == null) continue; micros[k] = (micros[k] ?? 0) + v; } } if (Object.keys(micros).length > 0) sum.micro = micros; return sum; } export function sumNutrition(items: NutritionValues[]): NutritionValues { return items.reduce(addNutrition, { ...EMPTY_NUTRITION }); } /** Avrunda för presentation (heltal kcal, en decimal på gram). */ export function roundNutrition(values: NutritionValues): NutritionValues { const r1 = (v: number) => Math.round(v * 10) / 10; const rounded: NutritionValues = { kcal: Math.round(values.kcal), proteinG: r1(values.proteinG), carbsG: r1(values.carbsG), fatG: r1(values.fatG), saturatedFatG: r1(values.saturatedFatG), fiberG: r1(values.fiberG), sugarG: r1(values.sugarG), saltG: r1(values.saltG), }; if (values.micro) rounded.micro = values.micro; return rounded; } /** * Beräkna näring för en given mängd av en ingrediens/produkt utifrån dess * deklaration. Returnerar null när beräkningen inte kan göras säkert – * anroparen ansvarar då för att visa osäkerheten (spec §61.4), aldrig gissa. */ export function computeItemNutrition( quantity: number, unit: Unit, declaration: NutritionDeclaration, info: IngredientUnitInfo = {}, ): NutritionValues | null { if (quantity < 0) return null; switch (declaration.basis) { case "per_100_g": { const grams = toGrams(quantity, unit, info); if (grams == null) return null; return scaleNutrition(declaration.values, grams / 100); } case "per_100_ml": { const base = toBase(quantity, unit); let ml: number | null = null; if (base.kind === "volume") ml = base.amount; else { // massa/antal → gram → ml via densitet const grams = toGrams(quantity, unit, info); const density = info.densityGPerMl; if (grams != null && density != null && density > 0) ml = grams / density; } if (ml == null) return null; return scaleNutrition(declaration.values, ml / 100); } case "per_piece": { const base = toBase(quantity, unit); if (base.kind === "count") return scaleNutrition(declaration.values, base.amount); // Vikt angiven: räkna om via referensvikt. const grams = toGrams(quantity, unit, info); const ref = declaration.referenceWeightG; if (grams == null || ref == null || ref <= 0) return null; return scaleNutrition(declaration.values, grams / ref); } case "per_portion": { const base = toBase(quantity, unit); if (base.kind === "count") return scaleNutrition(declaration.values, base.amount); return null; } } }