Elixir List of Maps - Count particular field value

I am trying to count particular field value in a list of maps. Below is the map which I have.

I got this result from my ecto query.

    query   = from t in Tracks,
                where: t.admin_id == ^admin_id and t.destination == ^location_id,
                join: d in Driver, on: t.driver_id == d.id,
                join: c in Company, on: d.company_id == c.id,
                select: %{source: t.source, destination: t.destination, box_ids: t.box_ids, status: t.status, driver: d.name, company: c.name}

    Repo.all(query)
[
  %{
    company: "FEDEX",
    destination: "c6f072a7-5334-436f-a498-fa815871327a",
    driver: "David",
    source: "b27af8e4-656c-414a-9033-275ca8a888ef",
    status: 0
  },
  %{
    company: "FEDEX",
    destination: "c6f072a7-5334-436f-a498-fa815871327a",
    driver: "David",
    source: "b27af8e4-656c-414a-9033-275ca8a888ef",
    status: 1
  },
  %{
    company: "FEDEX",
    destination: "c6f072a7-5334-436f-a498-fa815871327a",
    driver: "David",
    source: "b27af8e4-656c-414a-9033-275ca8a888ef",
    status: 1
  }
]

In the above map i want to have to count of how many status 0 and how many has status 1.

Status 0 = pending
Status 1 = delivered

So I will have the result of

pending = 1
delivered = 2

I am trying with Enum.reduce but I could not able to achieve the result

  	pending   	= Enum.reduce(track_level, 0, fn x, acc -> 
  														if x.status == 0 do
  															acc + 1
														end  														 
  													end)

Can anyone give me insight on how to achieve this? Your help is greatly appreciated.

The key thing with reduce is that you have to return an accumulator each time (or it gets reset to nil). In your code above you don’t have an else branch so you’re not returning the unchanged accumulator.

I would use a tuple to track the pending and delivered counts and pattern matching to decide which one to update. This way you can do it in one pass through the list.

{pending, delivered} = Enum.reduce(track_level, {0,0}, fn
  %{status: 0}, {pending, delivered} -> {pending + 1, delivered}
  %{status: 1}, {pending, delivered} -> {pending, delivered + 1}
end)
6 Likes

You might also use Enum.group_by… on status. This will group pending, delivered together.

iex> Enum.group_by track_level, & &1.status
%{
  0 => [
    %{
      company: "FEDEX",
      destination: "c6f072a7-5334-436f-a498-fa815871327a",
      driver: "David",
      source: "b27af8e4-656c-414a-9033-275ca8a888ef",
      status: 0
    }
  ],
  1 => [
    %{
      company: "FEDEX",
      destination: "c6f072a7-5334-436f-a498-fa815871327a",
      driver: "David",
      source: "b27af8e4-656c-414a-9033-275ca8a888ef",
      status: 1
    },
    %{
      company: "FEDEX",
      destination: "c6f072a7-5334-436f-a498-fa815871327a",
      driver: "David",
      source: "b27af8e4-656c-414a-9033-275ca8a888ef",
      status: 1
    }
  ]
}
3 Likes

Thanks a lot, @chrismcg. I learnt new things from your answer.

For now i am using kokolegorille answer.
Since it’s based on group by I don’t have to loop anything.

So performance-wise group by is better than looping right?

  	new_map = Enum.group_by(track_level, & &1.status) 

  	pending   = length(new_map[0])
  	delivered = length(new_map[1])
1 Like

If you have just two status types you can also use https://hexdocs.pm/elixir/Enum.html#split_with/2

2 Likes

The answer is always to benchmark :slight_smile: Though first use whichever code is clearest to you and you’ll still understand in 6 months. Only optimise if you’ve proven it’s a bottleneck.

I think the reduce one would be faster because it only goes through the list once. The group by has to go through it once to group, and then each length has to go through the list again to find its size so you’re going though the list twice.

I would guess that this is unlikely to make a difference in practise because the time taken to go to the database and get the data will be far greater than the time taken to go through the list once vs twice. I know for sure that guessing performance stuff is wrong most of the time so always measure! (With e.g. https://github.com/bencheeorg/benchee)

5 Likes

Why not use Enum.count/2?

pending = Enum.count(records, & &1.status == 0)

Or refactor so you can perform the count in Ecto:

pending = Repo.aggregate(query |> where(status: 0), :count)
2 Likes