Grouping and extract values from list of maps

Hi,
I have a list of maps such as:

[%{x: a, y: 1}, %{x: a, y: 1}, %{x: b, y: 0}, %{x: b, y: 1}]

From this I would like to find out how many 1s a and b have respectively

ie. resulting in something like:

[a: 2, b:1]

(or something to that effect)

I have accomplished this using rather brute force and un-elixirish ways and do feel there should be a rather elegant solution but it has eluded me so far and any help is appreciated.

Are a and b variables? Your given code snippet is not valid Elixir otherwise. They could be atoms (:a and :b) or strings ("a" and "b").

2 Likes

What did you try already? What felt less-than-ideal to you about it?

I’d probably just start with an Enum.reduce and go from there, because this is pretty similar conceptually to the “count the occurrence of a letter or word in a string” problem that frequently appears in coding koans/challenges/exercises.

3 Likes

One, pretty straight-forward, way to accomplish what you’re after is to think in the steps of what you want.

  1. You want to group your list into keys and the values of each unique key
  2. You want to count the unique values (in your example, you want to count the 1s)

So you have something like this:

[%{x: :a, y: 1}, %{x: :a, y: 1}, %{x: :b, y: 0}, %{x: :b, y: 1}]
    |> Enum.group_by(fn %{x: x} -> x end, fn %{y: y} -> y end)
    |> IO.inspect(label: "grouped")
    |> Enum.reduce([], fn {key, values}, acc ->
      acc ++ [key, Enum.count(values, fn x -> x == 1 end)]
    end)

Now you may think this is very specific to finding the count of 1s. And you’re right.

To make it more general is just as easy.
Just think of the steps you need to take to get what you want.

It’s exactly as above with an extra step of finding the values of n. …and use some functions to be kind to others. :stuck_out_tongue:

defmodule Playground do
  def hello do
    [%{x: :a, y: 1}, %{x: :a, y: 1}, %{x: :b, y: 0}, %{x: :b, y: 1}]
    |> count_values()
    |> count_value(1)
    |> IO.inspect(label: "count of 1s by key")
  end

  defp count_values(list) do
    list
    |> Enum.group_by(fn %{x: x} -> x end, fn %{y: y} -> y end)
    |> IO.inspect(label: "values grouped by key")
    |> Enum.reduce([], fn {key, values}, acc ->
      counts =
        Enum.reduce(values, %{}, fn value, acc ->
          Map.update(acc, value, 1, &(&1 + 1))
        end)

      acc ++ [{key, counts}]
    end)
    |> IO.inspect(label: "values counted")
  end

  defp count_value(list, n) do
    Enum.reduce(list, [], fn {key, values}, acc ->
      count =
        values
        |> Enum.filter(fn {value, _} -> value == n end)
        |> Enum.map(fn {_, count} -> count end)
        |> IO.inspect(label: "#{key} #{n}s")

      # ensure we use zero if we have no results in `count`
      count =
        case count do
          [n] -> n
          _ -> 0
        end

      acc ++ [{key, count}]
    end)
  end
end

Now that you have your answers, clean it up however you feel is necessary.

Cheers!

1 Like

No. I simply meant something that there can be more than one of on which I need to sort. In the original data they are the numbers 1 and 2, which also would make a confusing question here. I should have pointed this out.

I’m not hugely experienced in Elixir yet and my (bad) solution is basically splitting it into two separate lists, transforming them and merging back together. You really don’t want to see the code :slight_smile:

Now I feel rather embarrassed. I simply missed that there was a count/2. In hindsight this is rather obvious in language like this.

2 Likes