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 recipeIngredients: List<RecipeIngredientInfo>,
val recipeInstructions: List<RecipeInstructionInfo>,
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(

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.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(

View File

@@ -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 <T> LifecycleOwner.collectWhenResumed(flow: Flow<T>, collector: FlowCollecto
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.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?) {

View File

@@ -19,7 +19,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
private val binding by viewBinding(FragmentRecipeInfoBinding::bind)
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() }
@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)

View File

@@ -12,4 +12,5 @@ data class RecipeInfoUiState(
val recipeInstructions: List<RecipeInstructionEntity> = emptyList(),
val title: 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() },
title = entity.recipeSummaryEntity.name,
description = entity.recipeSummaryEntity.description,
disableAmounts = entity.recipeEntity.disableAmounts,
)
} ?: RecipeInfoUiState()
emit(state)

View File

@@ -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<RecipeIngredientEntity, RecipeIngredientViewHolder>(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<RecipeIngredientEntity>() {
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<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.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