"""
Recipe analysis functionality.
"""
import re
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any, Union, Tuple
from ..client import FdcClient
from ..models import Food, Nutrient
from .analysis import analyze_food, NutrientAnalysis
from .dri import DriType, Gender
[docs]@dataclass
class Ingredient:
"""
Represents an ingredient in a recipe.
"""
food: Food
weight_g: float
description: Optional[str] = None
[docs] def __str__(self) -> str:
"""String representation of the ingredient."""
if self.description:
return f"{self.description} ({self.weight_g}g)"
return f"{self.food.description} ({self.weight_g}g)"
[docs]@dataclass
class Recipe:
"""
Represents a recipe with ingredients.
"""
name: str
ingredients: List[Ingredient]
servings: int = 1
description: Optional[str] = None
@property
def total_weight_g(self) -> float:
"""Get the total weight of the recipe in grams."""
return sum(ingredient.weight_g for ingredient in self.ingredients)
[docs] def get_weight_per_serving(self) -> float:
"""Get the weight per serving in grams."""
return self.total_weight_g / self.servings
[docs]@dataclass
class RecipeAnalysis:
"""
Analysis of a recipe's nutrient content.
"""
recipe: Recipe
per_serving_analysis: NutrientAnalysis
ingredient_analyses: List[NutrientAnalysis]
[docs]def parse_ingredient(
text: str,
client: FdcClient,
search_limit: int = 5
) -> Optional[Ingredient]:
"""
Parse an ingredient description into an Ingredient object.
Args:
text: The ingredient description (e.g., "1 cup flour").
client: The FDC client to use for food lookup.
search_limit: Maximum number of search results to consider.
Returns:
An Ingredient object, or None if parsing failed.
"""
# Extract quantity and unit if present
quantity_pattern = r'^([\d./]+)\s*([a-zA-Z]+)?\s+(.+)$'
match = re.match(quantity_pattern, text)
if match:
quantity_str, unit, food_name = match.groups()
# Parse quantity
try:
if '/' in quantity_str:
num, denom = quantity_str.split('/')
quantity = float(num) / float(denom)
else:
quantity = float(quantity_str)
except ValueError:
quantity = 1.0
# Default unit is piece/item if not specified
unit = unit or "piece"
else:
# No quantity/unit found, assume it's just a food name
quantity = 1.0
unit = "piece"
food_name = text
# Search for the food
search_results = client.search(food_name, page_size=search_limit)
if not search_results.foods:
return None
# Use the first search result
food_id = search_results.foods[0].fdc_id
food = client.get_food(food_id)
# Estimate weight in grams based on unit and quantity
weight_g = estimate_weight(food, quantity, unit)
return Ingredient(
food=food,
weight_g=weight_g,
description=text
)
def estimate_weight(food: Food, quantity: float, unit: str) -> float:
"""
Estimate the weight in grams based on the food, quantity, and unit.
Args:
food: The food object.
quantity: The quantity.
unit: The unit of measurement.
Returns:
The estimated weight in grams.
"""
# Check if the unit is already grams
if unit.lower() in ["g", "gram", "grams"]:
return quantity
# Check if the unit is kilograms
if unit.lower() in ["kg", "kilogram", "kilograms"]:
return quantity * 1000.0
# Check if the food has portions that match the unit
for portion in food.food_portions:
if portion.measure_unit and portion.measure_unit.lower() == unit.lower():
return quantity * portion.gram_weight
# Default weights for common units
unit_weights = {
"cup": 240.0,
"cups": 240.0,
"tbsp": 15.0,
"tablespoon": 15.0,
"tablespoons": 15.0,
"tsp": 5.0,
"teaspoon": 5.0,
"teaspoons": 5.0,
"oz": 28.35,
"ounce": 28.35,
"ounces": 28.35,
"lb": 453.59,
"pound": 453.59,
"pounds": 453.59,
"piece": 100.0,
"pieces": 100.0,
"item": 100.0,
"items": 100.0,
"slice": 30.0,
"slices": 30.0
}
# Use default weight if available
if unit.lower() in unit_weights:
return quantity * unit_weights[unit.lower()]
# If no match, assume 100g per unit
return quantity * 100.0
[docs]def create_recipe(
name: str,
ingredient_texts: List[str],
client: FdcClient,
servings: int = 1,
description: Optional[str] = None
) -> Recipe:
"""
Create a recipe from ingredient descriptions.
Args:
name: The name of the recipe.
ingredient_texts: List of ingredient descriptions.
client: The FDC client to use for food lookup.
servings: The number of servings the recipe makes.
description: Optional description of the recipe.
Returns:
A Recipe object.
"""
ingredients = []
for text in ingredient_texts:
ingredient = parse_ingredient(text, client)
if ingredient:
ingredients.append(ingredient)
return Recipe(
name=name,
ingredients=ingredients,
servings=servings,
description=description
)
[docs]def analyze_recipe(
recipe: Recipe,
dri_type: DriType = DriType.RDA,
gender: Gender = Gender.MALE,
age: int = 30
) -> RecipeAnalysis:
"""
Analyze the nutrient content of a recipe.
Args:
recipe: The recipe to analyze.
dri_type: The type of DRI to use for comparison.
gender: The gender to use for DRI values.
age: The age to use for DRI values.
Returns:
A RecipeAnalysis object.
"""
# Analyze each ingredient
ingredient_analyses = []
for ingredient in recipe.ingredients:
analysis = analyze_food(
ingredient.food,
serving_size=ingredient.weight_g,
dri_type=dri_type,
gender=gender,
age=age
)
ingredient_analyses.append(analysis)
# Create a combined food object for the entire recipe
combined_food = Food(
fdc_id=0,
description=recipe.name,
data_type="Recipe",
nutrients=[]
)
# Combine nutrients from all ingredients
nutrient_map: Dict[int, Nutrient] = {}
for analysis in ingredient_analyses:
for nutrient in analysis.food.nutrients:
if nutrient.id in nutrient_map:
# Add to existing nutrient
existing = nutrient_map[nutrient.id]
existing.amount += nutrient.amount * (analysis.serving_size / 100.0)
else:
# Create new nutrient
new_nutrient = Nutrient(
id=nutrient.id,
name=nutrient.name,
amount=nutrient.amount * (analysis.serving_size / 100.0),
unit_name=nutrient.unit_name,
nutrient_nbr=nutrient.nutrient_nbr,
rank=nutrient.rank
)
nutrient_map[nutrient.id] = new_nutrient
combined_food.nutrients.append(new_nutrient)
# Analyze the combined food per serving
per_serving_analysis = analyze_food(
combined_food,
serving_size=recipe.get_weight_per_serving(),
dri_type=dri_type,
gender=gender,
age=age
)
return RecipeAnalysis(
recipe=recipe,
per_serving_analysis=per_serving_analysis,
ingredient_analyses=ingredient_analyses
)