3
\$\begingroup\$

I was trying to write code that would get as input a recipe consisting of item materials and would create all possible recipes that could be used to create craftable of the recipe.

The Recipe is represented by this data class:

private data class RecipeTree(
    val materials: List<Material>,
    val craftableID: Long,
    val producedQty: Int,
    val subRecipes: List<Pair<RecipeTree, Int>>,
)

Recipe consists of (1) materials, (2) id of item that is creates using this recipe, (3) quantity of items produced by this recipe, list of sub-recipes that needed to be created beforehand + number of times the sub-recipe needed to be used - hence I used a pair of Recipe + Int.

Material is represented as a simple class of Item + Quantity required.

private data class Material(val item: Item,val qty: Int)

class Item(
  var name: String,

  var recipes: MutableSet<Recipe> = mutableSetOf(),

  var id: Long? = null
)

Given a RecipTree of the original recipe, I needed to create all possible recipes that can be used to create the desired craftable. For example, if the original recipe consisted of item1 and item5, with item5 having a recipe that uses item4 to create item5, then one possible recipe would be item1 + sub-recipe that uses item4 to create item5. This can go on and on, for example item 4 could have another recipe.

I created the following recursive extension function.
Extension function is a function with receiver - you can think of it as a function that has hidden parameter which is the object on which it is being called. In my case, hidden parameter is an object of type RecipeTree. This parameter is used when code uses variables not defined by function parameters or local variables. For example,in code materials.size is actually an access to the property of RecipeTree on which the extension is called.

fun createRecipe(recipeCreationDTO: RecipeCreationDTO){
  ....
       RecipeTree(recipeCreationDTO.materials.map { Material(materials[it.materialID]!!,it.quantity) },recipeCreationDTO.craftableID,recipeCreationDTO.producedQuantity, emptyList()).derivedRecipes(0)
....
}

The extenstion function:

private fun RecipeTree.derivedRecipes(index: Int) : List<RecipeTree>{
    if(index == materials.size - 1){
        val result = mutableListOf<RecipeTree>()
        result.add(RecipeTree(listOf(materials[index]),craftableID,producedQty, emptyList()))
        for(recipe in materials[index].item.recipes){
            val recipeQty = ceil(materials[index].qty / recipe.producedQuantity.toDouble()).toInt()
            val subRecipe = RecipeTree(recipe.materials().map { Material(it.item,it.quantity) },recipe.craftable.id!!,recipe.producedQuantity, emptyList())
            result.add(RecipeTree(emptyList(),craftableID,producedQty,listOf(subRecipe to recipeQty)))
            result.addAll(subRecipe.derivedRecipes(0).map { RecipeTree(emptyList(),craftableID,producedQty,listOf(RecipeTree(emptyList(),recipe.craftable.id!!,recipe.producedQuantity,listOf(it to recipeQty)) to recipeQty)) })
        }
        return result
    }

    val partialRecipes = derivedRecipes(index + 1)

    val result = mutableListOf<RecipeTree>()

    for(partialRecipe in partialRecipes){
        result.add(partialRecipe.copy(materials = partialRecipe.materials.plus(materials[index])))
        for(recipe in materials[index].item.recipes){
            val recipeQty = ceil(materials[index].qty / recipe.producedQuantity.toDouble()).toInt()
            val subRecipe = RecipeTree(recipe.materials().map { Material(it.item,it.quantity) },recipe.craftable.id!!,recipe.producedQuantity, emptyList())
            result.add(partialRecipe.copy(subRecipes = partialRecipe.subRecipes.plus(subRecipe to recipeQty)))
            result.addAll(subRecipe.derivedRecipes(0).map { partialRecipe.copy(subRecipes = partialRecipe.subRecipes.plus(it to recipeQty)) })
        }
    }

    return result
}

I will try to explain the code:

  1. I used a recursive function because at first I was trying to create a function that would create recipes with different combination of materials + recipes that could be used in place of the material. This was similar to generating all possible bit string combinations(00,01,11,10 for example) where 0 mean use of material and 1 used of recipe of the material, with the only difference that instead of having 0 and 1, i would have more values for each position since one material could have 10 recipes and another 5. So what I basically do to create all combinations of recipe consists of x materials, is i recursively create all possible combinations for a recipe consisting of x - 1 materials, and then I modify each recipe to add the material/sub-recipes of the current recursive call.
  2. When I reach the last index, I create all possible combinations of Recipe by creating (a) recipe that contains the material at specified index, (b) for each recipe of the material, I create a sub-recipe that is used as a replacement of the material in additional recipe, (c) for each sub-recipe that is used as a replacement of the material I create all possible combinations that can be used to replace it and create additional recipe for each that uses the sub-recipe of the material that uses the created sub-recipe.

I tried this code with the following test data and got 287 combinations.

Test Data:
Original Recipe:Item1,Item4.
Item1 recipe: item2.
Item2 recipe: item3.
Item3 recipe: item4,item5.
Item4 recipe: item5,item6.
Item5 recipe: item7.
Item7 recipe: item8,item9.

I need other people's opinion about the code, whether it has flaws/mistakes, maybe there are certain cases no covered, improvements/simpler solutions.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Sorry I haven't gotten too deep into your code so my review will be a bit shallow. I like when by just looking at data structures, I can clearly understand the point. From looking at your structures I don't see clear relationships between dataclasses.

  • RecipeTree is definitely confusing and overly complex class, that I am sure and needs rebuilding.
  • I find it strange that in RecipeTree you are using Pair<Something, Int>, where Int represents count of Something, yet soon afterwards you have Item and Material, I could see Material as Pair<Item, Int>. Choose one and stick with it.
  • Naming is bad imho. Is really Material X of Items in real life? If I see 2 classes named like that, this is not my intuitive conclusion.
  • Class Recipe referenced in your code is missing.
  • I feel like there is maybe room for polymorphism where sometimes there's a recipe, but sometimes there's already resource, that cannot be divided anymore, but these both entities can serve as resources for recipes to create new resources.

I could go on, but the main suggestion here is - take a deep breath, step back and really take time to define your structures, name them and define relationships between them. If you do that correctly, your code later will be much simpler just because of that.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.