84 lines
3.3 KiB
C#
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);
|
|
}
|