120 lines
4.0 KiB
TypeScript
120 lines
4.0 KiB
TypeScript
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<string, number | undefined>;
|
||
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;
|
||
}
|
||
}
|
||
}
|