# Performing multiple calculations on a nested struct

I’ve been trying to solve this but confused about how to accumulate each respective value. I would appreciate help. With the data structure below:

1. sum the `height` of line items with `type "a"` and multiply it with `price_a`.
2. sum the `width` of line items with `type "b"` and multiply it with `price_b`.

reduce the returned price results by summing them.

``````%SomeStruct{
price_a: 2,
price_b: 3,
line_items: [
%LineItems{
height: 5,
width: 8,
type: "a"
},
%LineItems{
height: 3,
width: 2,
type: "b"
},
%LineItems{
height: 4,
width: 6,
type: "a"
},
%LineItems{
height: 5,
width: 7,
type: "b"
}
]
}``````

@ion Try this one:

``````defmodule Example do
def sample(%{line_items: line_items, price_a: price_a, price_b: price_b}) do
line_items
|> Enum.reduce(%{a: 0, b: 0}, &do_sample/2)
|> Map.update!(:a, & &1 * price_a)
|> Map.update!(:b, & &1 * price_b)
end

defp do_sample(%{height: height, type: "a"}, acc), do: Map.update!(acc, :a, & &1 + height)

defp do_sample(%{type: "b", width: width}, acc), do: Map.update!(acc, :b, & &1 + width)
end

Example.sample(data)
# %{a: 18, b: 27}
``````

Extra tip! Here is more generic version:

``````defmodule Example do
@opts [a: :height, b: :width]

def sample(%{line_items: line_items} = data) do
map = Enum.reduce(line_items, %{}, &do_sample/2)
:maps.map(&do_sample(&1, &2, data), map)
end

defp do_sample(%{type: type} = item, acc) do
key = String.to_existing_atom(type)
value = Map.fetch!(item, @opts[key])
Map.update(acc, key, value, &(&1 + value))
end

defp do_sample(type, value, data), do: value * Map.fetch!(data, :"price_#{type}")
end
``````
2 Likes

Thank you for working that through.

1. In the `sample` function, what is stopping the `Enum.reduce/3` function from always returning 0 when the accumulator’s starting value for :a and :b are 0?
2. In the 3rd argument of `Enum.reduce/3`, you are passing `&do_sample/2`. What are the arguments that are passed to `&do_sample/2`? Are the arguments `line_items` and `%{a: 0, b: 0}`?
3. Do you have any learning resources/tutorials that explain exercises containing pattern matching in function heads and advanced uses of function capture operators?
4. I added attribute property and am trying to pattern match against like attribute values. Does this look correct?

New data structure

``````%SomeStruct{
price_a: 2,
price_b: 3,
line_items: [
%LineItems{
height: 5,
width: 8,
type: "a",
attribute_1: "w",
attribute_2: "x",
attribute_3: "w",
attribute_4: "x"
},
%LineItems{
height: 3,
width: 2,
type: "b",
attribute_1: "w",
attribute_2: "x",
attribute_3: "w",
attribute_4: "x"
},
%LineItems{
height: 4,
width: 6,
type: "a",
attribute_1: "w",
attribute_2: "x",
attribute_3: "w",
attribute_4: "x"
},
%LineItems{
height: 5,
width: 7,
type: "b",
attribute_1: "w",
attribute_2: "x",
attribute_3: "w",
attribute_4: "x"
},
%LineItems{
height: 4,
width: 7,
type: "a",
attribute_1: "w",
attribute_2: "x",
attribute_3: "w",
attribute_4: "x"
},
%LineItems{
height: 2,
width: 6,
type: "b",
attribute_1: "w",
attribute_2: "x",
attribute_3: "w",
attribute_4: "x"
},
%LineItems{
height: 8,
width: 4,
type: "a",
attribute_1: "w",
attribute_2: "x",
attribute_3: "w",
attribute_4: "x"
},
%LineItems{
height: 3,
width: 3,
type: "b",
attribute_1: "w",
attribute_2: "x",
attribute_3: "w",
attribute_4: "x"
}
]
}
``````

New code

``````  def sample(%{line_items: line_items, price_a: price_a, price_b: price_b}) do
line_items
|> Enum.reduce(%{type_a: 0, type_b: 0}, &do_sample/2)
|> Map.update!(:type_a, &(&1 * price_a))
|> Map.update!(:type_b, &(&1 * price_b))

end

defp do_sample(%{width: width, attribute_1: "w"}, acc), do: Map.update!(acc, :type_a, & &1 + width)
defp do_sample(%{height: height, attribute_2: "w"}, acc), do: Map.update!(acc, :type_a, & &1 + height)
defp do_sample(%{width: width, attribute_3: "w"}, acc), do: Map.update!(acc, :type_a, & &1 + width)
defp do_sample(%{height: height, attribute_4: "w"}, acc), do: Map.update!(acc, :type_a, & &1 + height)

defp do_sample(%{width: width, attribute_1: "x"}, acc), do: Map.update!(acc, :type_b, & &1 + width)
defp do_sample(%{height: height, attribute_2: "x"}, acc), do: Map.update!(acc, :type_b, & &1 + height)
defp do_sample(%{width: width, attribute_3: "x"}, acc), do: Map.update!(acc, :type_b, & &1 + width)
defp do_sample(%{height: height, attribute_4: "x"}, acc), do: Map.update!(acc, :type_b, & &1 + height)``````

Nothing is stopping `Enum.reduce/3`. `Enum.reduce/3` is stopping itself simply when it finished iterating enumerable.

`&fun_name/2` is shorter version of: `fn first_arg, second_arg -> fun_name(first_arg, second_arg) end`. Arguments are passing by `Enum.reduce/3` implementation.

Here we have `{key, value}` in first argument as a `Keyword` or `Map` iteration. This means that `Enum.reduce/3` is taking `key-value` pairs and is calling specified function for each of them.

Second argument is accumulator. As name says it accumulates specified function return. You can set default value (for first iterated pair) in 2nd argument of `Enum.reduce/3`. For 2nd iteration `acc` value is value returned from 1st iteration etc.

For better description please take a look at Enum.reduce/3 documentation.

Pattern matching is pretty simple. It just tries each declaration and tries to match specified arguments. Let’s say that we have 2 arguments. If first match requires `Integer`, but we will provide any other declaration then it’s simply does not matched and there is called next check for next declaration.

``````def something(first, 5) when is_integer(first), do: :ok
def something(first, second) when is_integer(first) and second != 5, do: :second_is_not_five
def something(first, 5), do: :first_is_not_integer
def something(_first, _second), do: :first_is_not_integer_and_second_is_not_five

something(5, 5) # :ok
something(5, 6) #  :second_is_not_five
something("5", 5) #  :first_is_not_integer
something("5", 6) #  :first_is_not_integer_and_second_is_not_five
``````

There is lots of resources. Search at forum, read documentation and google it.

and many, many more …

I don’t know what are you trying to do. As long as code compiles and you did not provide me what result you expect then for me it’s working, because it does not raises any error.

Note: Your first head pattern is: `%{width: width, attribute_1: "w"}`. This means that it would match all `line_items` with `attribute` key set to `w` and which also have `width` key (so pattern can bind it to your variable). In your case every item in `line_items` matches first pattern and there is no need to check next ones as it’s already valid and follows firstly validated match. It’s why you have `0` value in `b` key. If you do not want to always match first pattern then you need to modify it in way that some passed data would not match it and therefore next pattern would be checked.

2 Likes

Thank you for the explanations, and the resources. I will definitely be reviewing the Programming Elixir book again.

What I am trying to do now is the following:

1. sum the `height` of line items with values of `w` for `attribute_2` or `attribute_4` and multiply it with price_w.
2. sum the `height` of line items with values of `x` for `attribute_2` or `attribute_4` and multiply it with price_x.
3. sum the `width` of line items with values of `w` for `attribute_1` or `attribute_3` and multiply it with price_w.
4. sum the `width` of line items with values of `x` for `attribute_1` or `attribute_3` and multiply it with price_x.

I updated the nested struct (renamed key names, and made the %LineItem singular)

``````%SomeStruct{
price_w: 2,
price_x: 3,
line_items: [
%LineItem{
height: 5,
width: 8,
attribute_1: "w",
attribute_2: "w",
attribute_3: "x",
attribute_4: "w"
},
%LineItem{
height: 3,
width: 2,
attribute_1: "x",
attribute_2: "x",
attribute_3: "w",
attribute_4: "w"
},
%LineItem{
height: 4,
width: 6,
attribute_1: "w",
attribute_2: "x",
attribute_3: "w",
attribute_4: "x"
},
%LineItem{
height: 5,
width: 7,
attribute_1: "x",
attribute_2: "x",
attribute_3: "x",
attribute_4: "x"
},
%LineItem{
height: 4,
width: 7,
attribute_1: "w",
attribute_2: "w",
attribute_3: "w",
attribute_4: "w"
},
%LineItem{
height: 2,
width: 6,
attribute_1: "w",
attribute_2: "w",
attribute_3: "w",
attribute_4: "x"
},
%LineItem{
height: 8,
width: 4,
attribute_1: "w",
attribute_2: "x",
attribute_3: "w",
attribute_4: "x"
},
%LineItem{
height: 3,
width: 3,
attribute_1: "w",
attribute_2: "x",
attribute_3: "x",
attribute_4: "w"
}
]
}
``````

Originally, I tried a data struct for %LineItem that looked like this:

``````%LineItem{
attribute_1: [width: 8, type: "w"]
attribute_2: [height: 5, type: "w"],
attribute_3: [width: 8, type: "x"],
attribute_4: [height: 5, type: "w]
}
``````

But I thought it is not good practice to repeat data, and instead handle it in the logic. I need to run a reducer on `attribute_#`, adding the widths or heights, and multiplying the sum of each with the price of `w` or `x`.

Hmm … now I see … You are not going to create a unique match, but instead a set of rules, so each item could match more than one rule.

I think that it could be much harder to maintain it in case you are writing such example data manually. Also the bigger problem is size in memory. Struct which you show of course is not problem, but I guess that’s only example. Not needed big data stored in memory for lots of users could cause really big memory problems. Fortunately in your example we are not including this into assumptions for simplicity.

Here is my generic solution:

``````defmodule SomeStruct do
defstruct [:line_items, :price_x, :price_w]
end

defmodule LineItem do
defstruct [:attribute_1, :attribute_2, :attribute_3, :attribute_4, :height, :width]
end

defmodule Example do
@rules [
attribute_1: [x: :width, w: :width],
attribute_2: [x: :height, w: :height],
attribute_3: [x: :width, w: :width],
attribute_4: [x: :height, w: :height]
]

def sample(%SomeStruct{line_items: line_items} = data) do
map = Enum.reduce(line_items, %{}, &do_sample/2)
:maps.map(&multiply(&1, &2, data), map)
end

defp do_sample(line_item, acc), do: Enum.reduce(@rules, acc, &check_rules(&1, &2, line_item))

defp check_rules({attr_name, opts}, acc, line_item),
do: Enum.reduce(opts, acc, &check_rule(&1, &2, attr_name, line_item))

defp check_rule({attr_value, value_key}, acc, attr_name, line_item) do
string_value = Atom.to_string(attr_value)

if Map.fetch!(line_item, attr_name) == string_value do
value = Map.fetch!(line_item, value_key)

acc
|> update_in([attr_name], &(&1 || %{}))
|> update_in([attr_name, attr_value], &((&1 || 0) + value))
else
acc
end
end

defp multiply(_attr_name, map, line_item), do: :maps.map(&do_multiply(&1, &2, line_item), map)

defp do_multiply(key, value, line_item), do: value * Map.fetch!(line_item, :"price_#{key}")
end
``````

Using this code you can do something like:

``````result = Example.sample(data)

# In order to your points here are sums:
first  = result.attribute_2.w + result.attribute_4.w
second = result.attribute_2.x + result.attribute_4.x
third  = result.attribute_1.w + result.attribute_3.w
fourth = result.attribute_1.x + result.attribute_3.x
``````

Maybe it’s not fastest version, but instead it’s really easy to change by simply updating rules. Also `Example.sample/1` is not returning exactly what you wanted. It returns almost finished data which is ready for final simple manipulations like sums which are calculated based on your description. In such way you can easily replace your sums or simply fix them in case you change your mind about which one you want to sum etc.

1 Like

Thank you. As a relative beginner to Elixir (compared to you), it’s amazing to see how elegantly you created a solution with Enum.reduce, accumulators, and function captures. I’ve been learning a lot running the debugger on it.

I have responded to @ion’s private message in which he noticed that for some data there could be `nil` values, so I would like to share also example how to fix that edge cases simply:

``````attr_1w = result && result[:attribute_1] && result[:attribute_1][:w] || 0
attr_1x = result && result[:attribute_1] && result[:attribute_1][:x] || 0
attr_2w = result && result[:attribute_2] && result[:attribute_2][:w] || 0
attr_2x = result && result[:attribute_2] && result[:attribute_2][:x] || 0
attr_3w = result && result[:attribute_3] && result[:attribute_3][:w] || 0
attr_3x = result && result[:attribute_3] && result[:attribute_3][:x] || 0
attr_4w = result && result[:attribute_4] && result[:attribute_4][:w] || 0
attr_4x = result && result[:attribute_4] && result[:attribute_4][:x] || 0

first  = attr_2w + attr_4w
second = attr_2x + attr_4x
third  = attr_1w + attr_3w
fourth = attr_1x + attr_3x
``````

Of course someone could write also function which for each attribute `attribute_1`-`attribute_4` and for each attribute value (here only `x` and `w`) we could check, create empty value map and also add `0` as default, so `result` therefore would contain all data. My code is only example how to solve some things as dynamic and configurable as possible without having huge blob of code.

1 Like