Reputation: 175
new to Elixir and functional programming in general. I am looking to merge a new item into a list of existing items. When the "key" of the new item is already present in the list, I need to update the corresponding item in the list, otherwise I add the new item to the list.
I've come up with the below, but it seems a little clunky, is there a better way to be doing this?
Much thanks!
defmodule Test.LineItem do
defstruct product_id: nil, quantity: nil
end
defmodule Test do
alias Test.LineItem
def main do
existing_items = [
%LineItem{product_id: 1, quantity: 123},
%LineItem{product_id: 2, quantity: 234},
%LineItem{product_id: 3, quantity: 345}
]
IO.puts "*** SHOULD BE 3 ITEMS, QUANTITY OF 123, 244, 345 ***"
new_item = %{product_id: 2, quantity: 10}
Enum.each merge(existing_items, new_item), &IO.inspect(&1)
IO.puts "*** SHOULD BE 4 ITEMS, QUANTITY OF 10, 123, 234, 345 ***"
new_item = %{product_id: 4, quantity: 10}
Enum.each merge(existing_items, new_item), &IO.inspect(&1)
:ok
end
def merge(existing_items, new_item) do
existing_items = existing_items |> Enum.map(&Map.from_struct/1)
lines = Enum.map(existing_items, fn(x) ->
if x.product_id == new_item.product_id do
%{product_id: x.product_id, quantity: x.quantity + new_item.quantity}
else
x
end
end)
unless Enum.find(lines, &(Map.get(&1, :product_id)==new_item.product_id)) do
[new_item | lines]
else
lines
end
end
end
Upvotes: 0
Views: 849
Reputation: 51369
Your solution is quite close. It can be cleaned up in a couple different ways:
Here is what I would do:
def merge(existing_items, new_item) do
if Enum.any?(existing_items, &(&1.product_id == new_item.product_id)) do
Enum.map(existing_items, fn existing_item ->
if existing_item.product_id == new_item.product_id do
%{existing_item | quantity: existing_item.quantify + new_item.quantity}
else
existing_item
end
end)
else
[new_item | existing_items]
end
end
The map update %{... | ...}
could be moved to its own function for clarity.
Upvotes: 1
Reputation: 1672
You could use maps for this.
map = %{
1 => %LineItem{product_id: 1, quantity: 123},
2 => %LineItem{product_id: 2, quantity: 234},
3 => %LineItem{product_id: 3, quantity: 345}
}
# update existing item:
item = %LineItem{product_id: 2, quantity: 10}
map = Map.update(map, item.product_id, item, fn old_item ->
%{old_item | quantity: old_item.quantity + item.quantity}
end)
# you can define a helper function so that you don't have to manually type the key
def upsert(map, %LineItem{} = item) do
Map.update(map, item.product_id, item, fn old_item ->
%{old_item | quantity: old_item.quantity + item.quantity}
end)
end
# insert new item:
item =%LineItem{product_id: 4, quantity: 10}
map = upsert(map, item)
Then if you need items as list you can just
Map.values(map)
But of course with this solution you end up duplicating ids as keys.
Upvotes: 0
Reputation: 2863
I suppose you don't have duplicate prodct_id
.
Not changing your struct, I recommend using List.update_at.
At first, use Enum.find_index
instead of Enum.find
to get the exist index(if there is), then just update it.
def merge(existing_items, new_item) do
existing_items = existing_items |> Enum.map(&Map.from_struct/1)
case Enum.find_index(existing_items, &(Map.get(&1, :product_id)==new_item.product_id)) do
nil ->
[new_item | existing_items]
index ->
List.update_at(existing_items, index, fn x ->
%{product_id: x.product_id, quantity: x.quantity + new_item.quantity}
end)
end
end
Upvotes: 0