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. :slight_smile: 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.

https://elixir-lang.org/getting-started/pattern-matching.html
https://elixirschool.com/en/lessons/basics/pattern-matching/

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