diff --git a/Proj.hs b/Proj.hs new file mode 100644 index 0000000..3ab0bb2 --- /dev/null +++ b/Proj.hs @@ -0,0 +1,157 @@ +{-# LANGUAGE OverloadedStrings #-} + +import Control.Exception +import Control.Monad +import Data.ByteString.Lazy qualified as BL +import Data.ByteString.Lazy.Char8 qualified as BLC +import Data.Csv +import Data.Function (on) +import Data.List (maximumBy) +import Data.Vector (Vector) +import Data.Vector qualified as V +import System.IO.Error (isDoesNotExistError) +import System.Random (randomRIO) +import Test.HUnit + +-- Definicja typu Dish (Danie) +data Dish = Dish + { dishName :: BLC.ByteString, + recipe :: BLC.ByteString, + calories :: Int + } + deriving (Show) + +-- Definicja instancji Eq dla Dish +instance Eq Dish where + (Dish name1 _ cal1) == (Dish name2 _ cal2) = name1 == name2 && cal1 == cal2 + +-- Definicja instancji FromNamedRecord dla Dish do parsowania z CSV +instance FromNamedRecord Dish where + parseNamedRecord r = + Dish + <$> r .: "mealName" + <*> r .: "mealRecipe" + <*> r .: "mealCalories" + +--instance FromNamedRecord Dish where +--parseNamedRecord r = do + --mealName <- r .: "mealName" + --mealRecipe <- r .: "mealRecipe" + --mealCalories <- r .: "mealCalories" + --return (Dish mealName mealRecipe mealCalories) + + +-- Definicja instancji ToNamedRecord dla Dish do zapisywania do CSV +instance ToNamedRecord Dish where + toNamedRecord (Dish name recipe cal) = + namedRecord ["mealName" .= name, "mealRecipe" .= recipe, "mealCalories" .= cal] + +-- Definicja instancji DefaultOrdered dla Dish do zachowania kolejności nagłówków w CSV +instance DefaultOrdered Dish where + headerOrder _ = header ["mealName", "mealRecipe", "mealCalories"] + +-- Wczytuje plik CSV i parsuje go na listę dań +readCSV :: FilePath -> IO [Dish] +readCSV path = do + contents <- BLC.readFile path + case decodeByName contents of + Left err -> do + putStrLn $ "Błąd parsowania CSV: " ++ err + return [] + Right (_, v) -> return $ V.toList v + +-- Zapisuje listę dań do pliku CSV +writeCSV :: FilePath -> [Dish] -> IO () +writeCSV path dishes = do + let encoded = encodeDefaultOrderedByName dishes + BL.writeFile path encoded + +-- Proponuje posiłki na podstawie podanej ilości kalorii +suggestMeals :: Int -> [Dish] -> IO (Dish, Dish, Dish) +suggestMeals _ [] = return (Dish "" "" 0, Dish "" "" 0, Dish "" "" 0) +suggestMeals targetCalories dishes = do + breakfast <- chooseRandomMeal $ filter (\d -> calories d <= targetCalories `div` 3) dishes + let remaining1 = filter (/= breakfast) dishes + let lunch = chooseBestMeal (targetCalories - calories breakfast) $ filter (\d -> calories d <= targetCalories `div` 2) remaining1 + let remaining2 = filter (/= lunch) remaining1 + let dinner = chooseBestMeal (targetCalories - calories breakfast - calories lunch) remaining2 + return (breakfast, lunch, dinner) + +-- Wybiera najlepsze danie na podstawie kalorii +chooseBestMeal :: Int -> [Dish] -> Dish +chooseBestMeal _ [] = Dish "" "" 0 +chooseBestMeal targetCalories meals = maximumBy (compare `on` calories) $ filter (\d -> calories d <= targetCalories) meals + +-- Wybiera losowe danie z listy +chooseRandomMeal :: [Dish] -> IO Dish +chooseRandomMeal [] = return $ Dish "" "" 0 +chooseRandomMeal meals = do + idx <- randomRIO (0, length meals - 1) + return $ meals !! idx + +-- Funkcja do wprowadzania nowych dań przez użytkownika +addNewDishes :: FilePath -> IO () +addNewDishes path = do + putStrLn "Ile dań chcesz dodać?" + n <- readLn + newDishes <- forM [1 .. n] $ \_ -> do + putStrLn "Podaj nazwę dania:" + name <- BLC.pack <$> getLine + putStrLn "Podaj przepis:" + recipe <- BLC.pack <$> getLine + putStrLn "Podaj ilość kalorii:" + cal <- readLn + return $ Dish name recipe cal + existingDishes <- readCSV path + let allDishes = existingDishes ++ newDishes + writeCSV path allDishes + putStrLn "Nowe dania zostały zapisane do pliku." + +-- Testy jednostkowe +testSuggestMeals :: Test +testSuggestMeals = + TestList + [ "Test suggestMeals for empty dish list" + ~: do + let csvFile = "empty.csv" + result <- tryJust (guard . isDoesNotExistError) $ readCSV csvFile + case result of + Left _ -> return () -- Test zakończy się powodzeniem, jeśli otrzymamy Left + Right _ -> assertFailure ("Test suggestMeals for empty dish list with file " ++ csvFile ++ ": should have failed to read CSV file"), + "Test suggestMeals for non-empty dish list" + ~: do + let csvFile = "baza.csv" + targetCalories = 1400 + result <- tryJust (guard . isDoesNotExistError) $ readCSV csvFile + case result of + Left _ -> assertFailure ("Test suggestMeals for non-empty dish list with file " ++ csvFile ++ ": could not read CSV file") + Right dishes -> do + (breakfast, lunch, dinner) <- suggestMeals targetCalories dishes + assertBool "Śniadanie nie powinno być puste" (dishName breakfast /= "" && calories breakfast > 0) + assertBool "Obiad nie powinien być pusty" (dishName lunch /= "" && calories lunch > 0) + assertBool "Kolacja nie powinna być pusta" (dishName dinner /= "" && calories dinner > 0) + ] + +main :: IO () +main = do + testResult <- runTestTT testSuggestMeals + putStrLn "Co chcesz zrobić? (1) Otrzymać menu na pewną ilość kalorii (2) Dodać nowe dania" + choice <- getLine + let csvFile = "baza.csv" + case choice of + "1" -> do + putStrLn "Podaj średnią ilość kalorii na dzień:" + targetCalories <- readLn + result <- tryJust (guard . isDoesNotExistError) $ readCSV csvFile + case result of + Left _ -> putStrLn $ "Plik " ++ csvFile ++ " nie istnieje." + Right dishes -> do + (breakfast, lunch, dinner) <- suggestMeals targetCalories dishes + putStrLn "Śniadanie:" + print breakfast + putStrLn "Obiad:" + print lunch + putStrLn "Kolacja:" + print dinner + "2" -> addNewDishes csvFile + _ -> putStrLn "Niepoprawny wybór."