Skip to content

Commit

Permalink
Merge pull request #5 from 4bitWise/dev
Browse files Browse the repository at this point in the history
feat: generate recipes based on monthly budget
  • Loading branch information
linerol authored Dec 11, 2024
2 parents ef9ecaf + 0b236f9 commit fe4ec5f
Show file tree
Hide file tree
Showing 9 changed files with 2,650 additions and 1,766 deletions.
724 changes: 724 additions & 0 deletions data/new_recipes.json

Large diffs are not rendered by default.

3,510 changes: 1,755 additions & 1,755 deletions data/recipes.json

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions scripts/add_recipe_costs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
""" [TODO] implement a script to calculate recipes costs and add theme to each recipes
Returns the list of all recipes with all costs added
"""
import json
import sys
from typing import Any, Dict


measureUnits = dict()
ingredients = dict()
recipes = dict()

def readJsonFile(filepath: str) -> Dict:
# Open and read the file
with open(filepath, 'r', encoding='utf-8') as file:
data = json.load(file)
return data;

def writeJsonFile(filepath: str, data: dict) -> None:
# write data to file
with open(filepath, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)

def find_element_by_field_value(data: list[dict], field: str, value: Any) -> dict | None:
for item in data:
if item.get(field) == value:
return item
return None

def convertToBaseUnit(qty: int, unit_id: str) -> int:
unit = find_element_by_field_value(measureUnits, "id", unit_id)
if (not unit['base_unit_id']):
return qty
else:
return convertToBaseUnit(qty * unit['conversion_factor'], unit['base_unit_id'])

def calculateRecipeCost(recipe: dict) -> int:
totalCost = 0
for ingredientInfo in recipe['ingredients']:
qty = ingredientInfo["quantity"]
ingredientId = ingredientInfo['ingredient_id']
unitId = ingredientInfo["unit_id"]
try:
totalCost += find_element_by_field_value(ingredients, "id", ingredientId)['price'] * convertToBaseUnit(qty, unitId)
except TypeError as e:
# Handling the TypeError if the object is NoneType
if str(e) == "'NoneType' object is not subscriptable":
print("Error:")
print(recipe["id"] + f" at Ingredient: {ingredientId}")
raise
else:
print(e)
sys.exit(1)
return totalCost


def addCostToRecipes() -> Dict:
newRecipes = []
for recipe in recipes:
try:
cost = calculateRecipeCost(recipe)
newRecipes.append({**recipe, "cost": cost})
except:
None
return (newRecipes)



def main(av):
global measureUnits, ingredients, recipes

if (len(av) != 2):
print(f"Usage {av[0]} path_to_new_file")
sys.exit(0)
measureUnits = readJsonFile("./data/measure-units.json")
ingredients = readJsonFile("./data/ingredients.json")
recipes = readJsonFile("./data/recipes.json")
newRecipes = addCostToRecipes()
writeJsonFile(sys.argv[1], newRecipes)
return 0;


if __name__ == "__main__":
av = sys.argv
sys.exit(main(av))
8 changes: 1 addition & 7 deletions src/dtos/recipe/recipe.dto.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
IsString,
IsNumber,
IsArray,
ValidateNested,
IsPositive,
} from 'class-validator';
import { IsString, IsArray, ValidateNested, IsPositive } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, PartialType } from '@nestjs/swagger';

Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe);
app.useGlobalPipes(new ValidationPipe());
const config = new DocumentBuilder()
.setTitle('EcoMeal API Documentation')
.setDescription('Enjoy using this swaager ;-)')
Expand Down
3 changes: 3 additions & 0 deletions src/schemas/recipe/recipe.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export class Recipe {
@Prop({ required: true, type: Number })
servings: number;

@Prop({ required: true, type: Number })
cost: number;

@Prop({ required: true, type: [IngredientDetail] })
ingredients: IngredientDetail[];

Expand Down
10 changes: 8 additions & 2 deletions src/services/measureunit/measureunits.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { CreateMeasureunitDto, UpdateMeasureunitDto } from 'src/dtos/measureunit/measureunit.dto';
import { Measureunit, MeasureunitDocument } from 'src/schemas/measureunit/measureunit.schema';
import {
CreateMeasureunitDto,
UpdateMeasureunitDto,
} from 'src/dtos/measureunit/measureunit.dto';
import {
Measureunit,
MeasureunitDocument,
} from 'src/schemas/measureunit/measureunit.schema';

@Injectable()
export class MeasureunitsService {
Expand Down
33 changes: 32 additions & 1 deletion src/services/recipe/recipes.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class RecipesController {
}
}

@ApiBody({ type: IngredientDetailDto })
@ApiBody({ type: [String] })
@ApiOperation({ summary: 'Find all ingredients based on many recipes' })
@Post('generate-ingredients-list')
async generateIngredientsList(
Expand All @@ -119,4 +119,35 @@ export class RecipesController {
);
}
}

// [TODO] Implement
@ApiBody({
description: "Generate a list of recipes based on the user's budget",
required: true,
schema: {
type: 'object',
properties: {
budget: {
type: 'number',
example: 300,
description: 'Total budget for generating recipes',
},
},
},
})
@ApiOperation({ summary: 'Generate a list of recipes based on budget' })
@Post('generate')
async generateRecipes(@Body('budget') budget: number): Promise<Recipe[]> {
try {
return await this.recipesService.generateRecipesFromBudget(budget);
} catch (err) {
throw new HttpException(
{
status: HttpStatus.INTERNAL_SERVER_ERROR,
error: 'Error generating recipe list!' + err.stack,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
41 changes: 41 additions & 0 deletions src/services/recipe/recipes.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,45 @@ export class RecipesService {
}
return Object.values(ingredientsMap);
}

async generateRecipesFromBudget(budget: number): Promise<Recipe[]> {
if (budget === 0 || isNaN(budget)) {
throw new Error('Invalid per-meal budget calculated.');
}

const lowerBound = budget * 0.9;
const upperBound = budget * 1.1;

// Récupérer les recettes qui coûtent moins que le budget max
const allRecipes = await this.recipeModel
.find({ cost: { $lte: upperBound } })
.exec();

if (!allRecipes.length) {
throw new Error('Aucune recette ne correspond à ce budget.');
}

// Générer une combinaison de recettes
const result: Recipe[] = [];
let totalCost = 0;

while (
totalCost < lowerBound ||
(totalCost <= upperBound && Math.random() < 0.5)
) {
const randomRecipe =
allRecipes[Math.floor(Math.random() * allRecipes.length)];
result.push(randomRecipe);
totalCost += randomRecipe.cost;

// Si on dépasse le budget, on retire la dernière recette ajoutée
if (totalCost > upperBound) {
totalCost -= randomRecipe.cost;
result.pop();
break;
}
}

return allRecipes;
}
}

0 comments on commit fe4ec5f

Please sign in to comment.