riebeekn
riebeekn

Reputation: 175

Elixir, merging a new item into a list

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

Answers (3)

José Valim
José Valim

Reputation: 51369

Your solution is quite close. It can be cleaned up in a couple different ways:

  1. No need to convert from struct to map
  2. You can perform the find first

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

Ooba Elda
Ooba Elda

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)

Map.update/4 documentation

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

chris
chris

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

Related Questions