Files
DailyMeals/MealPlan.Api/Services/RecipeMacroCalculator.cs
2026-06-24 22:58:18 +02:00

84 lines
3.3 KiB
C#

using MealPlan.Api.Data;
using MealPlan.Api.Models;
using Microsoft.EntityFrameworkCore;
namespace MealPlan.Api.Services;
public readonly record struct MacroTotals(int Calories, decimal Protein, decimal Fat, decimal Carbs);
public static class RecipeMacroCalculator
{
public static bool IsSpiceUnit(string? unit)
{
if (string.IsNullOrWhiteSpace(unit)) return false;
var u = unit.Trim().ToLowerInvariant();
return u is "do smaku" or "optional" or "opcjonalnie" or "szczypta" or "to taste";
}
public static bool IsQuantifiedMacroIngredient(decimal? amountGrams, string? unit, int? catalogItemId) =>
catalogItemId.HasValue && amountGrams is > 0 && !IsSpiceUnit(unit);
public static async Task<MacroTotals?> ComputeFromCatalogAsync(
AppDbContext db,
IEnumerable<(decimal? AmountGrams, string? Unit, int? CatalogItemId)> ingredients,
CancellationToken ct = default)
{
var rows = ingredients.ToList();
var ids = rows
.Where(i => IsQuantifiedMacroIngredient(i.AmountGrams, i.Unit, i.CatalogItemId))
.Select(i => i.CatalogItemId!.Value)
.Distinct()
.ToList();
if (ids.Count == 0) return null;
var catalog = await db.IngredientNutritionCatalog.AsNoTracking()
.Where(i => ids.Contains(i.Id) && i.IsActive)
.ToDictionaryAsync(i => i.Id, ct);
int calories = 0;
decimal protein = 0, fat = 0, carbs = 0;
foreach (var ing in rows)
{
if (!IsQuantifiedMacroIngredient(ing.AmountGrams, ing.Unit, ing.CatalogItemId)) continue;
if (!catalog.TryGetValue(ing.CatalogItemId!.Value, out var cat)) continue;
var scale = ing.AmountGrams!.Value / 100m;
calories += (int)Math.Round(cat.CaloriesPer100G * scale);
protein += cat.ProteinGPer100G * scale;
fat += cat.FatGPer100G * scale;
carbs += cat.CarbsGPer100G * scale;
}
if (calories <= 0) return null;
return new MacroTotals(calories, Math.Round(protein, 2), Math.Round(fat, 2), Math.Round(carbs, 2));
}
public static decimal ResolveScaleFactor(int currentCalories, int targetCalories, int toleranceKcal)
{
if (currentCalories <= 0 || targetCalories <= 0) return 1m;
if (Math.Abs(currentCalories - targetCalories) <= toleranceKcal) return 1m;
return Math.Clamp((decimal)targetCalories / currentCalories, 0.25m, 4m);
}
public static void ScaleIngredientAmounts(
IList<Ingredient> ingredients,
decimal scaleFactor,
int toleranceKcal,
int targetCalories,
MacroTotals? currentTotals)
{
if (scaleFactor == 1m) return;
if (currentTotals is null || currentTotals.Value.Calories <= 0) return;
if (Math.Abs(currentTotals.Value.Calories - targetCalories) <= toleranceKcal) return;
foreach (var ing in ingredients)
{
if (!IsQuantifiedMacroIngredient(ing.AmountGrams, ing.Unit, ing.CatalogItemId)) continue;
ing.AmountGrams = Math.Round(ing.AmountGrams!.Value * scaleFactor, 1);
}
}
public static int CountUnlinkedMacroIngredients(IEnumerable<Ingredient> ingredients) =>
ingredients.Count(i => i.AmountGrams is > 0 && !IsSpiceUnit(i.Unit) && !i.CatalogItemId.HasValue);
}