Hey I have a list of maps, and those maps are items such:
[
%purchase{
amount: #Decimal<2.0>,
command: "buy",
id: 71,
price: #Decimal<90.394>,
},
%purchase{
amount: #Decimal<1.0>,
command: "buy",
id: 72,
price: #Decimal<90.394>,
}
]
As you can see the prices are the same but the amount isn’t how would you go through the list to merge the items with the same price to get something like this:
[
%purchase{
amount: #Decimal<3.0>,
command: "buy",
price: #Decimal<90.394>,
}
]
Hey @benonymus what have you tried so far? If you need help getting started, check out the functions in https://hexdocs.pm/elixir/Enum.html and see if there’s something that might like you group_by
the items with the same price, after which you could go through each price group and reduce
them into a single purchase.
Hey, I came up with 2 solutions so far:
for order <- buy do
Enum.reduce(
buy,
order,
fn x, y ->
cond do
Decimal.cmp(x.price, y.price) == :eq and x.id != y.id ->
new_amount = Decimal.add(x.amount, y.amount)
Map.put(y, :amount, new_amount)
true ->
y
end
end
)
end
|> Enum.uniq_by(fn x -> x.price end)
and
for order <- buy do
Enum.map(buy, fn x ->
cond do
Decimal.cmp(x.price, order.price) == :eq and x.id != order.id ->
new_amount = Decimal.add(x.amount, order.amount)
Map.put(order, :amount, new_amount)
true ->
order
end
end)
end
|> List.flatten()
|> Enum.uniq_by(fn x -> x.price end)
But I am having a hard time deciding which one to stick with
Edit:
I decided to stick with the reduce version, so I don’t need to flatten a list
Needlessly verbose IMO. Here’s how I went about it:
defmodule Stock do
def aggregate_by_price(items) do
items
|> Enum.group_by(&(Decimal.reduce(&1.price)))
|> Enum.map(fn({_price, items}) ->
Enum.reduce(items, fn(acc, item) ->
%{acc | amount: Decimal.add(acc.amount, item.amount)}
end)
end)
end
end
Then in iex
:
items = [
%{price: Decimal.from_float(90.394), amount: Decimal.from_float(2.0)},
%{price: Decimal.from_float(90.394), amount: Decimal.from_float(1.0)},
%{price: Decimal.from_float(17.64738), amount: Decimal.from_float(10.0)},
%{price: Decimal.from_float(90.394), amount: Decimal.from_float(5.0)},
%{price: Decimal.from_float(17.64738), amount: Decimal.from_float(13.0)}
]
Stock.aggregate_by_price(items)
This returns:
[
%{amount: #Decimal<8.0>, price: #Decimal<90.394>},
%{amount: #Decimal<23.0>, price: #Decimal<17.64738>}
]
Which seems to be what you need.
3 Likes
We can golf this slightly smaller by using Enum.reduce/2
which does the hd/tl
thing for you:
Enum.reduce(items, fn(acc, item) ->
%{acc | amount: Decimal.add(acc.amount, item.amount)}
end)
The only real concern with this approach I’m realizing is that it compares price based on term equality not Decimal.cmp. That may require manual grouping, but that shouldn’t be too bad.
Heh, always thought Enum.reduce/2
repeats the first element when iterating. Thanks for ridding me of that dumb illusion!
Oops, I actually thought of addressing this but forgot. We can get away with it by using Decimal.reduce/1 because sometimes even identical float values – but represented differently when passed to Decimal.from_float
– yield false
when compared with ==
(which I agree should not be done).
Edited above to reflect your remarks.