Parse ingredient amounts from V1 response

This commit is contained in:
Kirill Kamakin
2022-12-04 18:47:27 +01:00
parent 8e7ccbeca1
commit a628912557
17 changed files with 472 additions and 31 deletions

View File

@@ -6,10 +6,18 @@ data class FullRecipeInfo(
val recipeYield: String, val recipeYield: String,
val recipeIngredients: List<RecipeIngredientInfo>, val recipeIngredients: List<RecipeIngredientInfo>,
val recipeInstructions: List<RecipeInstructionInfo>, val recipeInstructions: List<RecipeInstructionInfo>,
val settings: RecipeSettingsInfo,
)
data class RecipeSettingsInfo(
val disableAmounts: Boolean,
) )
data class RecipeIngredientInfo( data class RecipeIngredientInfo(
val note: String, val note: String,
val quantity: Double?,
val unit: String?,
val food: String?,
) )
data class RecipeInstructionInfo( data class RecipeInstructionInfo(

View File

@@ -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.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo 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.recipes.network.RecipeSummaryInfo
import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo
import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity 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.GetRecipeIngredientResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 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.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1 import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
@@ -40,12 +42,16 @@ import java.util.*
fun FullRecipeInfo.toRecipeEntity() = RecipeEntity( fun FullRecipeInfo.toRecipeEntity() = RecipeEntity(
remoteId = remoteId, remoteId = remoteId,
recipeYield = recipeYield recipeYield = recipeYield,
disableAmounts = settings.disableAmounts,
) )
fun RecipeIngredientInfo.toRecipeIngredientEntity(remoteId: String) = RecipeIngredientEntity( fun RecipeIngredientInfo.toRecipeIngredientEntity(remoteId: String) = RecipeIngredientEntity(
recipeId = remoteId, recipeId = remoteId,
note = note, note = note,
unit = unit,
food = food,
quantity = quantity,
) )
fun RecipeInstructionInfo.toRecipeInstructionEntity(remoteId: String) = RecipeInstructionEntity( fun RecipeInstructionInfo.toRecipeInstructionEntity(remoteId: String) = RecipeInstructionEntity(
@@ -114,11 +120,15 @@ fun GetRecipeResponseV0.toFullRecipeInfo() = FullRecipeInfo(
name = name, name = name,
recipeYield = recipeYield, recipeYield = recipeYield,
recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() }, recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() },
recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() } recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() },
settings = RecipeSettingsInfo(disableAmounts = true)
) )
fun GetRecipeIngredientResponseV0.toRecipeIngredientInfo() = RecipeIngredientInfo( fun GetRecipeIngredientResponseV0.toRecipeIngredientInfo() = RecipeIngredientInfo(
note = note, note = note,
unit = null,
food = null,
quantity = 1.0,
) )
fun GetRecipeInstructionResponseV0.toRecipeInstructionInfo() = RecipeInstructionInfo( fun GetRecipeInstructionResponseV0.toRecipeInstructionInfo() = RecipeInstructionInfo(
@@ -130,11 +140,19 @@ fun GetRecipeResponseV1.toFullRecipeInfo() = FullRecipeInfo(
name = name, name = name,
recipeYield = recipeYield, recipeYield = recipeYield,
recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() }, 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( fun GetRecipeIngredientResponseV1.toRecipeIngredientInfo() = RecipeIngredientInfo(
note = note, note = note,
unit = unit?.name,
food = food?.name,
quantity = quantity,
) )
fun GetRecipeInstructionResponseV1.toRecipeInstructionInfo() = RecipeInstructionInfo( fun GetRecipeInstructionResponseV1.toRecipeInstructionInfo() = RecipeInstructionInfo(

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.content.res.Resources
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
@@ -21,6 +22,7 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.viewbinding.ViewBinding
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.channels.ChannelResult import kotlinx.coroutines.channels.ChannelResult
@@ -132,4 +134,7 @@ fun <T> LifecycleOwner.collectWhenResumed(flow: Flow<T>, collector: FlowCollecto
flow.collect(collector) flow.collect(collector)
} }
} }
} }
val <T : ViewBinding> T.resources: Resources
get() = root.resources

View File

@@ -4,6 +4,7 @@ import androidx.recyclerview.widget.RecyclerView
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
import gq.kirmanak.mealient.extensions.resources
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
import javax.inject.Inject import javax.inject.Inject
@@ -30,7 +31,7 @@ class RecipeViewHolder private constructor(
} }
private val loadingPlaceholder by lazy { 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?) { fun bind(item: RecipeSummaryEntity?) {

View File

@@ -19,7 +19,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
private val binding by viewBinding(FragmentRecipeInfoBinding::bind) private val binding by viewBinding(FragmentRecipeInfoBinding::bind)
private val viewModel by viewModels<RecipeInfoViewModel>() private val viewModel by viewModels<RecipeInfoViewModel>()
private val ingredientsAdapter by lazy { recipeIngredientsAdapterFactory.build() } private lateinit var ingredientsAdapter: RecipeIngredientsAdapter
private val instructionsAdapter by lazy { recipeInstructionsAdapterFactory.build() } private val instructionsAdapter by lazy { recipeInstructionsAdapterFactory.build() }
@Inject @Inject
@@ -48,7 +48,6 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
logger.v { "onViewCreated() called" } logger.v { "onViewCreated() called" }
with(binding) { with(binding) {
ingredientsList.adapter = ingredientsAdapter
instructionsList.adapter = instructionsAdapter instructionsList.adapter = instructionsAdapter
} }
@@ -59,6 +58,10 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
private fun onUiStateChange(uiState: RecipeInfoUiState) = with(binding) { private fun onUiStateChange(uiState: RecipeInfoUiState) = with(binding) {
logger.v { "onUiStateChange() called" } logger.v { "onUiStateChange() called" }
if (::ingredientsAdapter.isInitialized.not()) {
ingredientsAdapter = recipeIngredientsAdapterFactory.build(uiState.disableAmounts)
ingredientsList.adapter = ingredientsAdapter
}
ingredientsHolder.isVisible = uiState.showIngredients ingredientsHolder.isVisible = uiState.showIngredients
instructionsGroup.isVisible = uiState.showInstructions instructionsGroup.isVisible = uiState.showInstructions
recipeImageLoader.loadRecipeImage(image, uiState.summaryEntity) recipeImageLoader.loadRecipeImage(image, uiState.summaryEntity)

View File

@@ -12,4 +12,5 @@ data class RecipeInfoUiState(
val recipeInstructions: List<RecipeInstructionEntity> = emptyList(), val recipeInstructions: List<RecipeInstructionEntity> = emptyList(),
val title: String? = null, val title: String? = null,
val description: String? = null, val description: String? = null,
val disableAmounts: Boolean = true,
) )

View File

@@ -29,6 +29,7 @@ class RecipeInfoViewModel @Inject constructor(
recipeInstructions = entity.recipeInstructions.filter { it.text.isNotBlank() }, recipeInstructions = entity.recipeInstructions.filter { it.text.isNotBlank() },
title = entity.recipeSummaryEntity.name, title = entity.recipeSummaryEntity.name,
description = entity.recipeSummaryEntity.description, description = entity.recipeSummaryEntity.description,
disableAmounts = entity.recipeEntity.disableAmounts,
) )
} ?: RecipeInfoUiState() } ?: RecipeInfoUiState()
emit(state) emit(state)

View File

@@ -2,6 +2,7 @@ package gq.kirmanak.mealient.ui.recipes.info
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -15,6 +16,7 @@ import javax.inject.Singleton
class RecipeIngredientsAdapter private constructor( class RecipeIngredientsAdapter private constructor(
private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory, private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory,
private val logger: Logger, private val logger: Logger,
private val disableAmounts: Boolean,
) : ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) { ) : ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) {
@Singleton @Singleton
@@ -22,12 +24,17 @@ class RecipeIngredientsAdapter private constructor(
private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory, private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory,
private val logger: Logger, private val logger: Logger,
) { ) {
fun build() = RecipeIngredientsAdapter(recipeIngredientViewHolderFactory, logger) fun build(disableAmounts: Boolean) = RecipeIngredientsAdapter(
recipeIngredientViewHolderFactory = recipeIngredientViewHolderFactory,
logger = logger,
disableAmounts = disableAmounts,
)
} }
class RecipeIngredientViewHolder private constructor( class RecipeIngredientViewHolder private constructor(
private val binding: ViewHolderIngredientBinding, private val binding: ViewHolderIngredientBinding,
private val logger: Logger, private val logger: Logger,
private val disableAmounts: Boolean,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@Singleton @Singleton
@@ -35,25 +42,37 @@ class RecipeIngredientsAdapter private constructor(
private val logger: Logger, private val logger: Logger,
) { ) {
fun build(binding: ViewHolderIngredientBinding) = fun build(
RecipeIngredientViewHolder(binding, logger) binding: ViewHolderIngredientBinding,
disableAmounts: Boolean,
) = RecipeIngredientViewHolder(
binding = binding,
logger = logger,
disableAmounts = disableAmounts,
)
} }
fun bind(item: RecipeIngredientEntity) { fun bind(item: RecipeIngredientEntity) {
logger.v { "bind() called with: item = $item" } 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<RecipeIngredientEntity>() { private object RecipeIngredientDiffCallback : DiffUtil.ItemCallback<RecipeIngredientEntity>() {
override fun areItemsTheSame( override fun areItemsTheSame(
oldItem: RecipeIngredientEntity, oldItem: RecipeIngredientEntity, newItem: RecipeIngredientEntity
newItem: RecipeIngredientEntity
): Boolean = oldItem.localId == newItem.localId ): Boolean = oldItem.localId == newItem.localId
override fun areContentsTheSame( override fun areContentsTheSame(
oldItem: RecipeIngredientEntity, oldItem: RecipeIngredientEntity, newItem: RecipeIngredientEntity
newItem: RecipeIngredientEntity
): Boolean = oldItem == newItem ): Boolean = oldItem == newItem
} }
@@ -61,7 +80,8 @@ class RecipeIngredientsAdapter private constructor(
logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" } logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return recipeIngredientViewHolderFactory.build( 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" } logger.d { "onBindViewHolder: item is $item" }
holder.bind(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<Int, Int, Int> {
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)
} }

View File

@@ -8,6 +8,7 @@ import androidx.recyclerview.widget.RecyclerView
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding
import gq.kirmanak.mealient.extensions.resources
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder
import javax.inject.Inject import javax.inject.Inject
@@ -52,7 +53,7 @@ class RecipeInstructionsAdapter private constructor(
fun bind(item: RecipeInstructionEntity, position: Int) { fun bind(item: RecipeInstructionEntity, position: Int) {
logger.v { "bind() called with: item = $item, position = $position" } 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 R.string.view_holder_recipe_instructions_step, position + 1
) )
binding.instruction.text = item.text binding.instruction.text = item.text

View File

@@ -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.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo 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.recipes.network.RecipeSummaryInfo
import gq.kirmanak.mealient.database.recipe.entity.* import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity
import gq.kirmanak.mealient.datasource.v0.models.* import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity
import gq.kirmanak.mealient.datasource.v1.models.* 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 gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
@@ -74,14 +97,23 @@ object RecipeImplTestData {
val SUGAR_INGREDIENT = RecipeIngredientInfo( val SUGAR_INGREDIENT = RecipeIngredientInfo(
note = "2 oz of white sugar", note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
) )
val BREAD_INGREDIENT = RecipeIngredientInfo( val BREAD_INGREDIENT = RecipeIngredientInfo(
note = "2 oz of white bread", note = "2 oz of white bread",
quantity = 1.0,
unit = null,
food = null,
) )
private val MILK_INGREDIENT = RecipeIngredientInfo( private val MILK_INGREDIENT = RecipeIngredientInfo(
note = "2 oz of white milk", note = "2 oz of white milk",
quantity = 1.0,
unit = null,
food = null,
) )
val MIX_INSTRUCTION = RecipeInstructionInfo( val MIX_INSTRUCTION = RecipeInstructionInfo(
@@ -101,7 +133,8 @@ object RecipeImplTestData {
name = "Cake", name = "Cake",
recipeYield = "4 servings", recipeYield = "4 servings",
recipeIngredients = listOf(SUGAR_INGREDIENT, BREAD_INGREDIENT), 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( val PORRIDGE_FULL_RECIPE_INFO = FullRecipeInfo(
@@ -109,7 +142,8 @@ object RecipeImplTestData {
name = "Porridge", name = "Porridge",
recipeYield = "3 servings", recipeYield = "3 servings",
recipeIngredients = listOf(SUGAR_INGREDIENT, MILK_INGREDIENT), 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( val MIX_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
@@ -124,17 +158,24 @@ object RecipeImplTestData {
val CAKE_RECIPE_ENTITY = RecipeEntity( val CAKE_RECIPE_ENTITY = RecipeEntity(
remoteId = "1", remoteId = "1",
recipeYield = "4 servings" recipeYield = "4 servings",
disableAmounts = true,
) )
val CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( val CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "1", recipeId = "1",
note = "2 oz of white sugar", note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
) )
val CAKE_BREAD_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( val CAKE_BREAD_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "1", recipeId = "1",
note = "2 oz of white bread", note = "2 oz of white bread",
quantity = 1.0,
unit = null,
food = null,
) )
val FULL_CAKE_INFO_ENTITY = FullRecipeEntity( val FULL_CAKE_INFO_ENTITY = FullRecipeEntity(
@@ -152,17 +193,24 @@ object RecipeImplTestData {
private val PORRIDGE_RECIPE_ENTITY_FULL = RecipeEntity( private val PORRIDGE_RECIPE_ENTITY_FULL = RecipeEntity(
remoteId = "2", remoteId = "2",
recipeYield = "3 servings" recipeYield = "3 servings",
disableAmounts = true,
) )
private val PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( private val PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "2", recipeId = "2",
note = "2 oz of white milk", note = "2 oz of white milk",
quantity = 1.0,
unit = null,
food = null,
) )
private val PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( private val PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "2", recipeId = "2",
note = "2 oz of white sugar", note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
) )
private val PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( 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 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") val MIX_RECIPE_INSTRUCTION_RESPONSE_V0 = GetRecipeInstructionResponseV0("Mix the ingredients")
@@ -295,6 +358,7 @@ object RecipeImplTestData {
MIX_RECIPE_INSTRUCTION_RESPONSE_V1, MIX_RECIPE_INSTRUCTION_RESPONSE_V1,
BOIL_RECIPE_INSTRUCTION_RESPONSE_V1 BOIL_RECIPE_INSTRUCTION_RESPONSE_V1
), ),
settings = GetRecipeSettingsResponseV1(disableAmount = true),
) )
val MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V0 = AddRecipeInstructionV0("Mix the ingredients") val MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V0 = AddRecipeInstructionV0("Mix the ingredients")

View File

@@ -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<Double, Int, Boolean>,
private val output: Triple<Int, Int, Int>,
) {
@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<Array<Any>> {
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)),
)
}
}
}

View File

@@ -28,7 +28,13 @@ class RecipeInfoViewModelTest : BaseUnitTest() {
@Test @Test
fun `when recipe is found then UI state has data`() = runTest { 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( val returnedEntity = FULL_CAKE_INFO_ENTITY.copy(
recipeIngredients = FULL_CAKE_INFO_ENTITY.recipeIngredients + emptyNoteIngredient recipeIngredients = FULL_CAKE_INFO_ENTITY.recipeIngredients + emptyNoteIngredient
) )

View File

@@ -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')"
]
}
}

View File

@@ -6,7 +6,7 @@ import gq.kirmanak.mealient.database.recipe.RecipeDao
import gq.kirmanak.mealient.database.recipe.entity.* import gq.kirmanak.mealient.database.recipe.entity.*
@Database( @Database(
version = 6, version = 7,
entities = [ entities = [
RecipeSummaryEntity::class, RecipeSummaryEntity::class,
RecipeEntity::class, RecipeEntity::class,
@@ -19,6 +19,7 @@ import gq.kirmanak.mealient.database.recipe.entity.*
AutoMigration(from = 3, to = 4), AutoMigration(from = 3, to = 4),
AutoMigration(from = 4, to = 5, spec = AppDb.From4To5Migration::class), AutoMigration(from = 4, to = 5, spec = AppDb.From4To5Migration::class),
AutoMigration(from = 5, to = 6, spec = AppDb.From5To6Migration::class), AutoMigration(from = 5, to = 6, spec = AppDb.From5To6Migration::class),
AutoMigration(from = 6, to = 7),
] ]
) )
@TypeConverters(RoomTypeConverters::class) @TypeConverters(RoomTypeConverters::class)

View File

@@ -8,4 +8,5 @@ import androidx.room.PrimaryKey
data class RecipeEntity( data class RecipeEntity(
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: String, @PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: String,
@ColumnInfo(name = "recipe_yield") val recipeYield: String, @ColumnInfo(name = "recipe_yield") val recipeYield: String,
@ColumnInfo(name = "disable_amounts", defaultValue = "true") val disableAmounts: Boolean,
) )

View File

@@ -9,6 +9,9 @@ data class RecipeIngredientEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "recipe_id") val recipeId: String, @ColumnInfo(name = "recipe_id") val recipeId: String,
@ColumnInfo(name = "note") val note: 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@@ -18,6 +21,9 @@ data class RecipeIngredientEntity(
if (recipeId != other.recipeId) return false if (recipeId != other.recipeId) return false
if (note != other.note) 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 return true
} }
@@ -25,6 +31,9 @@ data class RecipeIngredientEntity(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = recipeId.hashCode() var result = recipeId.hashCode()
result = 31 * result + note.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 return result
} }
} }

View File

@@ -8,13 +8,32 @@ data class GetRecipeResponseV1(
@SerialName("id") val remoteId: String, @SerialName("id") val remoteId: String,
@SerialName("name") val name: String, @SerialName("name") val name: String,
@SerialName("recipeYield") val recipeYield: String = "", @SerialName("recipeYield") val recipeYield: String = "",
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponseV1>, @SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponseV1> = emptyList(),
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponseV1>, @SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponseV1> = emptyList(),
@SerialName("settings") val settings: GetRecipeSettingsResponseV1,
)
@Serializable
data class GetRecipeSettingsResponseV1(
@SerialName("disableAmount") val disableAmount: Boolean,
) )
@Serializable @Serializable
data class GetRecipeIngredientResponseV1( data class GetRecipeIngredientResponseV1(
@SerialName("note") val note: String = "", @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 @Serializable