Best way to either add a new item to a list, or updating an existing one

I have a shopping cart that is represented by a list of products that are modeled as such: %{product_id: 123, quantity: 5}

When adding a new item to the shopping cart I want to update an existing product’s quantity if it already exists, otherwise I want to add the new product to the list. Pretty straightforward, but what I’ve come up with doesn’t seem ideal. The code I currently have is:

defp consolidate_line_items(new_item, existing_items) do
  item_exists = Enum.any? existing_items, fn item ->
    item.product_id == new_item.product_id
  end
  if item_exists do
    Enum.map existing_items, fn existing_item ->
      if existing_item.product_id == new_item.product_id do
        %{existing_item | quantity: new_item.quantity + existing_item.quantity}
      else
        existing_item
      end
    end
  else
    [new_item | existing_items]
  end
end

Currently I am traversing the list twice: once to see if the item exists, and then once to update it. The Enum.any? predicate is also repeated inside of the map. I feel like there is probably a way to avoid traversing the list twice.

One thing I could do is use map_reduce, using the map to update the item if it exists and using a boolean as the accumulator to keep track of whether or not the item has been found (which I can then use to determine if I need to prepend the new item or not). This gets me down to just a single pass over the cart. It would then look like:

defp consolidate_line_items(new_item, existing_items) do
  {items, found} = Enum.map_reduce existing_items, false, fn (existing_item, found) ->
      if existing_item.product_id == new_item.product_id do
        updated_item = %{existing_item | quantity: new_item.quantity + existing_item.quantity}
        {updated_item, true}
      else
        {existing_item, found}
      end
  end
  if found do
    items
  else
    [new_item | existing_items]
  end
end

I still feel like there is probably a simpler solution. Any ideas?

(Also, unrelated, but why doesn’t the second code block get syntax highlighting? )

1 Like

Naive functional and recursive approach:

def insert_item([], new_item), do: [new_item]
def insert_item([h = %{product_id: id, quantity: old_quantity}|t], %{product_id: id, quantity: growth}), do: [%{h | quantity: old_quantity + growth}|t]
def insert_item([h|t], new_item), do: [h|insert_item(t, new_item)]

You should be able to recreate similar behaviour using Enum.reduce/3 (will need a full scan always) or Enum.reduce_while/3 (I’m not quite sure if you can append the tail easily on a match). Nope you won’t :wink:

2 Likes

Ah, excellent. Definitely more concise, thank you. I always forget about being able to pattern match on previously bound parameters like you’re doing here with product_id.