From a628912557e7a442466aef0a89d44ca9016e4caf Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 4 Dec 2022 18:47:27 +0100 Subject: [PATCH] Parse ingredient amounts from V1 response --- .../data/recipes/network/FullRecipeInfo.kt | 8 + .../mealient/extensions/ModelMappings.kt | 24 ++- .../mealient/extensions/ViewExtensions.kt | 7 +- .../mealient/ui/recipes/RecipeViewHolder.kt | 3 +- .../ui/recipes/info/RecipeInfoFragment.kt | 7 +- .../ui/recipes/info/RecipeInfoUiState.kt | 1 + .../ui/recipes/info/RecipeInfoViewModel.kt | 1 + .../recipes/info/RecipeIngredientsAdapter.kt | 99 +++++++++- .../recipes/info/RecipeInstructionsAdapter.kt | 3 +- .../mealient/test/RecipeImplTestData.kt | 84 +++++++- .../ui/recipes/info/MediantMethodTest.kt | 37 ++++ .../recipes/info/RecipeInfoViewModelTest.kt | 8 +- .../7.json | 185 ++++++++++++++++++ .../gq/kirmanak/mealient/database/AppDb.kt | 3 +- .../database/recipe/entity/RecipeEntity.kt | 1 + .../recipe/entity/RecipeIngredientEntity.kt | 9 + .../v1/models/GetRecipeResponseV1.kt | 23 ++- 17 files changed, 472 insertions(+), 31 deletions(-) create mode 100644 app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/MediantMethodTest.kt create mode 100644 database/schemas/gq.kirmanak.mealient.database.AppDb/7.json diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/FullRecipeInfo.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/FullRecipeInfo.kt index 756e4ac..83a8628 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/FullRecipeInfo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/FullRecipeInfo.kt @@ -6,10 +6,18 @@ data class FullRecipeInfo( val recipeYield: String, val recipeIngredients: List, val recipeInstructions: List, + val settings: RecipeSettingsInfo, +) + +data class RecipeSettingsInfo( + val disableAmounts: Boolean, ) data class RecipeIngredientInfo( val note: String, + val quantity: Double?, + val unit: String?, + val food: String?, ) data class RecipeInstructionInfo( diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/ModelMappings.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/ModelMappings.kt index cf6e27c..256b0ab 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/ModelMappings.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/ModelMappings.kt @@ -8,6 +8,7 @@ import gq.kirmanak.mealient.data.baseurl.VersionInfo import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo +import gq.kirmanak.mealient.data.recipes.network.RecipeSettingsInfo import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity @@ -31,6 +32,7 @@ import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSettingsResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1 import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 @@ -40,12 +42,16 @@ import java.util.* fun FullRecipeInfo.toRecipeEntity() = RecipeEntity( remoteId = remoteId, - recipeYield = recipeYield + recipeYield = recipeYield, + disableAmounts = settings.disableAmounts, ) fun RecipeIngredientInfo.toRecipeIngredientEntity(remoteId: String) = RecipeIngredientEntity( recipeId = remoteId, note = note, + unit = unit, + food = food, + quantity = quantity, ) fun RecipeInstructionInfo.toRecipeInstructionEntity(remoteId: String) = RecipeInstructionEntity( @@ -114,11 +120,15 @@ fun GetRecipeResponseV0.toFullRecipeInfo() = FullRecipeInfo( name = name, recipeYield = recipeYield, recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() }, - recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() } + recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() }, + settings = RecipeSettingsInfo(disableAmounts = true) ) fun GetRecipeIngredientResponseV0.toRecipeIngredientInfo() = RecipeIngredientInfo( note = note, + unit = null, + food = null, + quantity = 1.0, ) fun GetRecipeInstructionResponseV0.toRecipeInstructionInfo() = RecipeInstructionInfo( @@ -130,11 +140,19 @@ fun GetRecipeResponseV1.toFullRecipeInfo() = FullRecipeInfo( name = name, recipeYield = recipeYield, recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() }, - recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() } + recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() }, + settings = settings.toRecipeSettingsInfo(), +) + +private fun GetRecipeSettingsResponseV1.toRecipeSettingsInfo() = RecipeSettingsInfo( + disableAmounts = disableAmount, ) fun GetRecipeIngredientResponseV1.toRecipeIngredientInfo() = RecipeIngredientInfo( note = note, + unit = unit?.name, + food = food?.name, + quantity = quantity, ) fun GetRecipeInstructionResponseV1.toRecipeInstructionInfo() = RecipeInstructionInfo( diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt index 15fc126..722ab6d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.SharedPreferences import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.content.res.Resources import android.os.Build import android.view.View import android.view.inputmethod.InputMethodManager @@ -21,6 +22,7 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.viewbinding.ViewBinding import com.google.android.material.textfield.TextInputLayout import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.channels.ChannelResult @@ -132,4 +134,7 @@ fun LifecycleOwner.collectWhenResumed(flow: Flow, collector: FlowCollecto flow.collect(collector) } } -} \ No newline at end of file +} + +val T.resources: Resources + get() = root.resources \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt index b3d5880..898bf5b 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt @@ -4,6 +4,7 @@ import androidx.recyclerview.widget.RecyclerView import gq.kirmanak.mealient.R import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding +import gq.kirmanak.mealient.extensions.resources import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader import javax.inject.Inject @@ -30,7 +31,7 @@ class RecipeViewHolder private constructor( } private val loadingPlaceholder by lazy { - binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder) + binding.resources.getString(R.string.view_holder_recipe_text_placeholder) } fun bind(item: RecipeSummaryEntity?) { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt index aac4ad7..d6649ef 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt @@ -19,7 +19,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() { private val binding by viewBinding(FragmentRecipeInfoBinding::bind) private val viewModel by viewModels() - private val ingredientsAdapter by lazy { recipeIngredientsAdapterFactory.build() } + private lateinit var ingredientsAdapter: RecipeIngredientsAdapter private val instructionsAdapter by lazy { recipeInstructionsAdapterFactory.build() } @Inject @@ -48,7 +48,6 @@ class RecipeInfoFragment : BottomSheetDialogFragment() { logger.v { "onViewCreated() called" } with(binding) { - ingredientsList.adapter = ingredientsAdapter instructionsList.adapter = instructionsAdapter } @@ -59,6 +58,10 @@ class RecipeInfoFragment : BottomSheetDialogFragment() { private fun onUiStateChange(uiState: RecipeInfoUiState) = with(binding) { logger.v { "onUiStateChange() called" } + if (::ingredientsAdapter.isInitialized.not()) { + ingredientsAdapter = recipeIngredientsAdapterFactory.build(uiState.disableAmounts) + ingredientsList.adapter = ingredientsAdapter + } ingredientsHolder.isVisible = uiState.showIngredients instructionsGroup.isVisible = uiState.showInstructions recipeImageLoader.loadRecipeImage(image, uiState.summaryEntity) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoUiState.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoUiState.kt index 7e3f812..00de3cd 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoUiState.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoUiState.kt @@ -12,4 +12,5 @@ data class RecipeInfoUiState( val recipeInstructions: List = emptyList(), val title: String? = null, val description: String? = null, + val disableAmounts: Boolean = true, ) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt index 0238e85..043b0b0 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt @@ -29,6 +29,7 @@ class RecipeInfoViewModel @Inject constructor( recipeInstructions = entity.recipeInstructions.filter { it.text.isNotBlank() }, title = entity.recipeSummaryEntity.name, description = entity.recipeSummaryEntity.description, + disableAmounts = entity.recipeEntity.disableAmounts, ) } ?: RecipeInfoUiState() emit(state) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeIngredientsAdapter.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeIngredientsAdapter.kt index fad0dc6..8e26acb 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeIngredientsAdapter.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeIngredientsAdapter.kt @@ -2,6 +2,7 @@ package gq.kirmanak.mealient.ui.recipes.info import android.view.LayoutInflater import android.view.ViewGroup +import androidx.annotation.VisibleForTesting import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -15,6 +16,7 @@ import javax.inject.Singleton class RecipeIngredientsAdapter private constructor( private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory, private val logger: Logger, + private val disableAmounts: Boolean, ) : ListAdapter(RecipeIngredientDiffCallback) { @Singleton @@ -22,12 +24,17 @@ class RecipeIngredientsAdapter private constructor( private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory, private val logger: Logger, ) { - fun build() = RecipeIngredientsAdapter(recipeIngredientViewHolderFactory, logger) + fun build(disableAmounts: Boolean) = RecipeIngredientsAdapter( + recipeIngredientViewHolderFactory = recipeIngredientViewHolderFactory, + logger = logger, + disableAmounts = disableAmounts, + ) } class RecipeIngredientViewHolder private constructor( private val binding: ViewHolderIngredientBinding, private val logger: Logger, + private val disableAmounts: Boolean, ) : RecyclerView.ViewHolder(binding.root) { @Singleton @@ -35,25 +42,37 @@ class RecipeIngredientsAdapter private constructor( private val logger: Logger, ) { - fun build(binding: ViewHolderIngredientBinding) = - RecipeIngredientViewHolder(binding, logger) + fun build( + binding: ViewHolderIngredientBinding, + disableAmounts: Boolean, + ) = RecipeIngredientViewHolder( + binding = binding, + logger = logger, + disableAmounts = disableAmounts, + ) } fun bind(item: RecipeIngredientEntity) { logger.v { "bind() called with: item = $item" } - binding.checkBox.text = item.note + binding.checkBox.text = if (disableAmounts) { + item.note + } else { + val builder = StringBuilder() + item.quantity?.let { builder.append("${it.formatUsingMediantMethod()} ") } + item.unit?.let { builder.append("$it ") } + item.food?.let { builder.append("$it ") } + builder.toString().trim() + } } } private object RecipeIngredientDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: RecipeIngredientEntity, - newItem: RecipeIngredientEntity + oldItem: RecipeIngredientEntity, newItem: RecipeIngredientEntity ): Boolean = oldItem.localId == newItem.localId override fun areContentsTheSame( - oldItem: RecipeIngredientEntity, - newItem: RecipeIngredientEntity + oldItem: RecipeIngredientEntity, newItem: RecipeIngredientEntity ): Boolean = oldItem == newItem } @@ -61,7 +80,8 @@ class RecipeIngredientsAdapter private constructor( logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" } val inflater = LayoutInflater.from(parent.context) return recipeIngredientViewHolderFactory.build( - ViewHolderIngredientBinding.inflate(inflater, parent, false) + ViewHolderIngredientBinding.inflate(inflater, parent, false), + disableAmounts, ) } @@ -71,4 +91,65 @@ class RecipeIngredientsAdapter private constructor( logger.d { "onBindViewHolder: item is $item" } holder.bind(item) } +} + +private fun Double.formatUsingMediantMethod(d: Int = 10, mixed: Boolean = true): String { + val triple = mediantMethod(d, mixed) + return when { + triple.first == 0 -> "${triple.second}/${triple.third}" + triple.second == 0 -> "${triple.first}" + else -> "${triple.first} ${triple.second}/${triple.third}" + } +} + +/** + * Rational approximation to a floating point number with bounded denominator using Mediant Method. + * For example, 333/1000 will become [0, 1, 3] (1/3), 1414/1000 will be [1, 2, 5] (1 2/5). + * Uses algorithm from this npm package - https://www.npmjs.com/package/frac + * Can be seen here https://github.com/SheetJS/frac/blob/d07f3c99c7dc059fb47d391bcb3da80f4956608e/frac.js + * @receiver - number that needs to be approximated + * @param d - maximum denominator (i.e. if 10 then 17/20 will be 4/5, if 20 then 17/20). + * @param mixed - if true returns a mixed fraction otherwise improper (i.e. "7/5" or "1 2/5") + */ +@VisibleForTesting +fun Double.mediantMethod(d: Int = 10, mixed: Boolean = true): Triple { + val x = this + var n1 = x.toInt() + var d1 = 1 + var n2 = n1 + 1 + var d2 = 1 + if (x != n1.toDouble()) { + while (d1 <= d && d2 <= d) { + val m = (n1 + n2).toDouble() / (d1 + d2) + when { + x == m -> { + when { + d1 + d2 <= d -> { + d1 += d2 + n1 += n2 + d2 = d + 1 + } + d1 > d2 -> d2 = d + 1 + else -> d1 = d + 1 + } + break + } + x < m -> { + n2 += n1 + d2 += d1 + } + else -> { + n1 += n2 + d1 += d2 + } + } + } + } + if (d1 > d) { + d1 = d2 + n1 = n2 + } + if (!mixed) return Triple(0, n1, d1) + val q = (n1.toDouble() / d1).toInt() + return Triple(q, n1 - q * d1, d1) } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInstructionsAdapter.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInstructionsAdapter.kt index 1ebb47a..0fe7b9b 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInstructionsAdapter.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInstructionsAdapter.kt @@ -8,6 +8,7 @@ import androidx.recyclerview.widget.RecyclerView import gq.kirmanak.mealient.R import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding +import gq.kirmanak.mealient.extensions.resources import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder import javax.inject.Inject @@ -52,7 +53,7 @@ class RecipeInstructionsAdapter private constructor( fun bind(item: RecipeInstructionEntity, position: Int) { logger.v { "bind() called with: item = $item, position = $position" } - binding.step.text = binding.root.resources.getString( + binding.step.text = binding.resources.getString( R.string.view_holder_recipe_instructions_step, position + 1 ) binding.instruction.text = item.text diff --git a/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt b/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt index 8b1107f..d7aac50 100644 --- a/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt +++ b/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt @@ -8,10 +8,33 @@ import gq.kirmanak.mealient.data.baseurl.VersionInfo import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo +import gq.kirmanak.mealient.data.recipes.network.RecipeSettingsInfo import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo -import gq.kirmanak.mealient.database.recipe.entity.* -import gq.kirmanak.mealient.datasource.v0.models.* -import gq.kirmanak.mealient.datasource.v1.models.* +import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeSettingsV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeIngredientResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeInstructionResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeIngredientV1 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1 +import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSettingsResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1 import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime @@ -74,14 +97,23 @@ object RecipeImplTestData { val SUGAR_INGREDIENT = RecipeIngredientInfo( note = "2 oz of white sugar", + quantity = 1.0, + unit = null, + food = null, ) val BREAD_INGREDIENT = RecipeIngredientInfo( note = "2 oz of white bread", + quantity = 1.0, + unit = null, + food = null, ) private val MILK_INGREDIENT = RecipeIngredientInfo( note = "2 oz of white milk", + quantity = 1.0, + unit = null, + food = null, ) val MIX_INSTRUCTION = RecipeInstructionInfo( @@ -101,7 +133,8 @@ object RecipeImplTestData { name = "Cake", recipeYield = "4 servings", recipeIngredients = listOf(SUGAR_INGREDIENT, BREAD_INGREDIENT), - recipeInstructions = listOf(MIX_INSTRUCTION, BAKE_INSTRUCTION) + recipeInstructions = listOf(MIX_INSTRUCTION, BAKE_INSTRUCTION), + settings = RecipeSettingsInfo(disableAmounts = true) ) val PORRIDGE_FULL_RECIPE_INFO = FullRecipeInfo( @@ -109,7 +142,8 @@ object RecipeImplTestData { name = "Porridge", recipeYield = "3 servings", recipeIngredients = listOf(SUGAR_INGREDIENT, MILK_INGREDIENT), - recipeInstructions = listOf(MIX_INSTRUCTION, BOIL_INSTRUCTION) + recipeInstructions = listOf(MIX_INSTRUCTION, BOIL_INSTRUCTION), + settings = RecipeSettingsInfo(disableAmounts = true) ) val MIX_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( @@ -124,17 +158,24 @@ object RecipeImplTestData { val CAKE_RECIPE_ENTITY = RecipeEntity( remoteId = "1", - recipeYield = "4 servings" + recipeYield = "4 servings", + disableAmounts = true, ) val CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( recipeId = "1", note = "2 oz of white sugar", + quantity = 1.0, + unit = null, + food = null, ) val CAKE_BREAD_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( recipeId = "1", note = "2 oz of white bread", + quantity = 1.0, + unit = null, + food = null, ) val FULL_CAKE_INFO_ENTITY = FullRecipeEntity( @@ -152,17 +193,24 @@ object RecipeImplTestData { private val PORRIDGE_RECIPE_ENTITY_FULL = RecipeEntity( remoteId = "2", - recipeYield = "3 servings" + recipeYield = "3 servings", + disableAmounts = true, ) private val PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( recipeId = "2", note = "2 oz of white milk", + quantity = 1.0, + unit = null, + food = null, ) private val PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( recipeId = "2", note = "2 oz of white sugar", + quantity = 1.0, + unit = null, + food = null, ) private val PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( @@ -253,11 +301,26 @@ object RecipeImplTestData { val SUGAR_RECIPE_INGREDIENT_RESPONSE_V0 = GetRecipeIngredientResponseV0("2 oz of white sugar") - val MILK_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1("2 oz of white milk") + val MILK_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1( + note = "2 oz of white milk", + quantity = 1.0, + unit = null, + food = null, + ) - val SUGAR_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1("2 oz of white sugar") + val SUGAR_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1( + note = "2 oz of white sugar", + quantity = 1.0, + unit = null, + food = null, + ) - val MILK_RECIPE_INGREDIENT_INFO = RecipeIngredientInfo("2 oz of white milk") + val MILK_RECIPE_INGREDIENT_INFO = RecipeIngredientInfo( + note = "2 oz of white milk", + quantity = 1.0, + unit = null, + food = null, + ) val MIX_RECIPE_INSTRUCTION_RESPONSE_V0 = GetRecipeInstructionResponseV0("Mix the ingredients") @@ -295,6 +358,7 @@ object RecipeImplTestData { MIX_RECIPE_INSTRUCTION_RESPONSE_V1, BOIL_RECIPE_INSTRUCTION_RESPONSE_V1 ), + settings = GetRecipeSettingsResponseV1(disableAmount = true), ) val MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V0 = AddRecipeInstructionV0("Mix the ingredients") diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/MediantMethodTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/MediantMethodTest.kt new file mode 100644 index 0000000..7b7ab44 --- /dev/null +++ b/app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/MediantMethodTest.kt @@ -0,0 +1,37 @@ +package gq.kirmanak.mealient.ui.recipes.info + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class MediantMethodTest( + private val input: Triple, + private val output: Triple, +) { + + @Test + fun `when mediantMethod is called expect correct result`() { + assertThat(input.first.mediantMethod(input.second, input.third)).isEqualTo(output) + } + + companion object { + @Parameters + @JvmStatic + fun parameters(): List> { + return listOf( + arrayOf(Triple(0.333, 10, true), Triple(0, 1, 3)), + arrayOf(Triple(0.333, 10, false), Triple(0, 1, 3)), + arrayOf(Triple(0.333, 100, false), Triple(0, 1, 3)), + arrayOf(Triple(0.333, 100, true), Triple(0, 1, 3)), + arrayOf(Triple(1.5, 10, true), Triple(1, 1, 2)), + arrayOf(Triple(1.5, 10, false), Triple(0, 3, 2)), + arrayOf(Triple(0.4, 10, false), Triple(0, 2, 5)), + arrayOf(Triple(0.41412412412412, 100, true), Triple(0, 41, 99)), + arrayOf(Triple(8.98, 10, true), Triple(9, 0, 1)), + ) + } + } +} \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModelTest.kt index c895e33..d81b033 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModelTest.kt @@ -28,7 +28,13 @@ class RecipeInfoViewModelTest : BaseUnitTest() { @Test fun `when recipe is found then UI state has data`() = runTest { - val emptyNoteIngredient = RecipeIngredientEntity(recipeId = "42", note = "") + val emptyNoteIngredient = RecipeIngredientEntity( + recipeId = "42", + note = "", + food = null, + unit = null, + quantity = 1.0, + ) val returnedEntity = FULL_CAKE_INFO_ENTITY.copy( recipeIngredients = FULL_CAKE_INFO_ENTITY.recipeIngredients + emptyNoteIngredient ) diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json new file mode 100644 index 0000000..c51d17b --- /dev/null +++ b/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json @@ -0,0 +1,185 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "2383ca8fe2fbd04ddaec6d7680de62ad", + "entities": [ + { + "tableName": "recipe_summaries", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "slug", + "columnName": "slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateAdded", + "columnName": "date_added", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateUpdated", + "columnName": "date_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageId", + "columnName": "image_id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, `disable_amounts` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipeYield", + "columnName": "recipe_yield", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disableAmounts", + "columnName": "disable_amounts", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "true" + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_ingredient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL, `food` TEXT, `unit` TEXT, `quantity` REAL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "food", + "columnName": "food", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unit", + "columnName": "unit", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quantity", + "columnName": "quantity", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_instruction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2383ca8fe2fbd04ddaec6d7680de62ad')" + ] + } +} \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt index d54cd7e..e801675 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt @@ -6,7 +6,7 @@ import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.entity.* @Database( - version = 6, + version = 7, entities = [ RecipeSummaryEntity::class, RecipeEntity::class, @@ -19,6 +19,7 @@ import gq.kirmanak.mealient.database.recipe.entity.* AutoMigration(from = 3, to = 4), AutoMigration(from = 4, to = 5, spec = AppDb.From4To5Migration::class), AutoMigration(from = 5, to = 6, spec = AppDb.From5To6Migration::class), + AutoMigration(from = 6, to = 7), ] ) @TypeConverters(RoomTypeConverters::class) diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt index e4784a2..9d0c60d 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt @@ -8,4 +8,5 @@ import androidx.room.PrimaryKey data class RecipeEntity( @PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: String, @ColumnInfo(name = "recipe_yield") val recipeYield: String, + @ColumnInfo(name = "disable_amounts", defaultValue = "true") val disableAmounts: Boolean, ) diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt index 4fcf844..440f73f 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt @@ -9,6 +9,9 @@ data class RecipeIngredientEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, @ColumnInfo(name = "recipe_id") val recipeId: String, @ColumnInfo(name = "note") val note: String, + @ColumnInfo(name = "food") val food: String?, + @ColumnInfo(name = "unit") val unit: String?, + @ColumnInfo(name = "quantity") val quantity: Double?, ) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -18,6 +21,9 @@ data class RecipeIngredientEntity( if (recipeId != other.recipeId) return false if (note != other.note) return false + if (food != other.food) return false + if (unit != other.unit) return false + if (quantity != other.quantity) return false return true } @@ -25,6 +31,9 @@ data class RecipeIngredientEntity( override fun hashCode(): Int { var result = recipeId.hashCode() result = 31 * result + note.hashCode() + result = 31 * result + (food?.hashCode() ?: 0) + result = 31 * result + (unit?.hashCode() ?: 0) + result = 31 * result + quantity.hashCode() return result } } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeResponseV1.kt index 87fdd31..9a37111 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeResponseV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeResponseV1.kt @@ -8,13 +8,32 @@ data class GetRecipeResponseV1( @SerialName("id") val remoteId: String, @SerialName("name") val name: String, @SerialName("recipeYield") val recipeYield: String = "", - @SerialName("recipeIngredient") val recipeIngredients: List, - @SerialName("recipeInstructions") val recipeInstructions: List, + @SerialName("recipeIngredient") val recipeIngredients: List = emptyList(), + @SerialName("recipeInstructions") val recipeInstructions: List = emptyList(), + @SerialName("settings") val settings: GetRecipeSettingsResponseV1, +) + +@Serializable +data class GetRecipeSettingsResponseV1( + @SerialName("disableAmount") val disableAmount: Boolean, ) @Serializable data class GetRecipeIngredientResponseV1( @SerialName("note") val note: String = "", + @SerialName("unit") val unit: GetRecipeIngredientUnitResponseV1?, + @SerialName("food") val food: GetRecipeIngredientFoodResponseV1?, + @SerialName("quantity") val quantity: Double?, +) + +@Serializable +data class GetRecipeIngredientFoodResponseV1( + @SerialName("name") val name: String = "", +) + +@Serializable +data class GetRecipeIngredientUnitResponseV1( + @SerialName("name") val name: String = "", ) @Serializable