Need help with this Elixir test for beginners

Hello, I’m starting in Elixir and I’m trying to perform a test but in reality I know I should learn more about it. I know that this question will be simple for some but in reality I need your guidance.

I want to do this exercise but I have not found the correct way to develop it. I appreciate the help.

You must build a function that receives a list with a set of lists, and you must group the first level by date and the second level with the time zone.

Given the list

[
[
"2018-12-01",
"AM",
"ID123",
5000
],
[
"2018-12-01",
"AM",
"ID545",
7000
],
[
"2018-12-01",
"PM",
"ID545",
3000
],
[
"2018-12-02",
"AM",
"ID545",
7000
]
]

You must generate the following map:

%{
"2018-12-01" => %{
"AM" => 12000,
"PM" => 3000
},
"2018-12-02" => %{
"AM" => 7000,
}
}

Where it should be consolidated (accumulate the value of the strip) in case it is repeated as

“2018-12-01” “AM”

Thank´s

As this is a learning exercise, I’m hesitant to give you a full working program. If you want someone to solve the problem for you just ask.

As a hint, let me suggest that you look at the documentation for Enum.group_by and List.first

From there you will probably want to know about Enum.map and Enum.reduce

3 Likes

Thanks for your time. I would really like to help you with this exercise and be part of a job and have not made progress. Surely if you help me to develop it, I will learn from the mistake I will be making. Thanks again.

1 Like

OK. I will start with an “.exs” file and declare a module to solve the problem. I’ll go ahead and put the data points into a module attribute and we’ll declare a function solve_problem() that we hope will solve the exercise (but for now we’ll just inspect the list).

defmodule Solution do
    @data_points [
      ["2018-12-01", "AM", "ID123", 5000],
      ["2018-12-01", "AM", "ID545", 7000],
      ["2018-12-01", "PM", "ID545", 3000],
      ["2018-12-02", "AM", "ID545", 7000]
    ]

    def solve_problem do
      @data_points
    end
end

IO.inspect(Solution.solve_problem())

First we want to separate the data points out and group them by date. We can use the Enum.group_by function to accomplish that to group the list of lists. The element we want to group by is the first item in the list so I’ll go ahead and use the List.first function to get at it. group_by can also transform the result so let’s drop the date from the collected data points:

    def data_points_by_date(data_points) do
      Enum.group_by(data_points, &List.first/1, &Enum.drop(&1, 1))
    end

The result of passing the data points to that function is:

%{
  "2018-12-01" => [
    ["AM", "ID123", 5000],
    ["AM", "ID545", 7000],
    ["PM", "ID545", 3000]
  ],
  "2018-12-02" => [["AM", "ID545", 7000]]
}

A map is a collection of key-value pairs. In this case the key is the date and the value is a list of data points. We want to group the data points in each value by their “meridians” (AM or PM). We can repeat the same use of Enum.group_by and List.first again on the values. In our group_by transform we might also collect the numbers from the list. Let’s do that with pattern matching on the lists of three values.

    …

    def group_by_meridian({date, data_points}) do
      {date, Enum.group_by(data_points, &List.first/1, fn [_, _, number] -> number end}
    end

    def solve_problem do
      @data_points
      |> data_points_by_date()
      |> Enum.into(%{}, &group_by_meridian/1)
    end

yields:

%{
  "2018-12-01" => %{"AM" => [5000, 7000], "PM" => [3000]},
  "2018-12-02" => %{"AM" => [7000]}
}

And we’re pretty close. We just need to sum up the values in the lists of numbers instead of returning the lists themselves. We can change group_by_meridian to do that using techniques we’ve already seen. We can build the map of meridians by pulling the expression out into a variable:

    def group_by_meridian({date, data_points}) do
      numbers_by_meridian = Enum.group_by(data_points, &List.first/1, fn([_, _, number]) -> number end)
      {date, numbers_by_meridian}
    end

(so numbers_by_meridian will look something like %{"AM" => [5000, 7000], "PM" => [3000])

Now we can use the trick of doing a Enum.map over the key value pairs in numbers_by_meridian where we change the list of numbers into sums. (I actually use Enum.into again which does the Enum.map under the covers). To calculate the sums we use Enum.reduce and pass in the binary function “+” (or Kernel.+). The function looks like this at first blush:

    def group_by_meridian({date, data_points}) do
      numbers_by_meridian = Enum.group_by(data_points, &List.first/1, fn([_, _, number]) -> number end)
      sums_by_meridian = Enum.into(numbers_by_meridian, %{}, fn {meridian, numbers} -> {meridian, Enum.reduce(numbers, 0, &+/2)} end)

      {date, sums_by_meridian}
    end

And we can clean up things a bit:

defmodule Solution do

    @data_points [
      ["2018-12-01", "AM", "ID123", 5000],
      ["2018-12-01", "AM", "ID545", 7000],
      ["2018-12-01", "PM", "ID545", 3000],
      ["2018-12-02", "AM", "ID545", 7000]
    ]

    # Take the data points and create a map grouping them by date
    def data_points_by_date(data_points) do
      Enum.group_by(data_points, &List.first/1, &Enum.drop(&1,1))
    end

    def group_by_meridian({date, data_points}) do
      sums_by_meridian = data_points
      |> Enum.group_by(&List.first/1, &extract_number/1)
      |> Enum.into(%{}, &sum_numbers/1)

      {date, sums_by_meridian}
    end

    def solve_problem do
      @data_points
      |> data_points_by_date()
      |> Enum.into(%{}, &group_by_meridian/1)
    end

    defp extract_number([_, _, number]), do: number
    defp sum_numbers({meridian, numbers_list}), do: {meridian, Enum.reduce(numbers_list, &+/2)}
end

IO.inspect(Solution.solve_problem())

yielding:

%{
  "2018-12-01" => %{"AM" => 12000, "PM" => 3000},
  "2018-12-02" => %{"AM" => 7000}
}
7 Likes

Thank you very much, you have really oriented me too much. This exercise, although it looked simple, applies a lot of knowledge about Elixir. Again thank you very much.

2 Likes

As a non-learning example but a more traditionally-written-example, it would usually be written like this:

iex(1)> [
...(1)>   ["2018-12-01", "AM", "ID123", 5000],
...(1)>   ["2018-12-01", "AM", "ID545", 7000],
...(1)>   ["2018-12-01", "PM", "ID545", 3000],
...(1)>   ["2018-12-02", "AM", "ID545", 7000]
...(1)> ] |> Enum.reduce(%{}, fn [date, clas, _id, time], acc ->
...(1)>   Map.update(acc, date, %{clas => time}, &Map.put(&1, clas, Map.get(&1, clas, 0) + time))
...(1)> end)
%{
  "2018-12-01" => %{"AM" => 12000, "PM" => 3000},
  "2018-12-02" => %{"AM" => 7000}
}

Essentially this just reduces over the input lists with an initial empty map as the output, for each entry in the list, which is 4-element lists it then matches over those and either adds it to the output list if the date doesn’t already exist or updates the existing date’s value if it does by adding it to the existing time value if it exists (or else just adds it to 0 if it doesn’t). :slight_smile:

Even if not writing it like this yet it is still good to understand how it works. It is using a reduce function , anonymous functions, and quick-syntax for anonymous functions (&blah(&1) == fn(arg1) -> blah(arg1) end). :slight_smile:

3 Likes

Speaking about learning - any “introduction to functional programming” that I’ve been exposed to has the following learning order:

  1. lists
  2. recursion
  3. implement foldl (reduce) in terms of recursion
  4. implement map in terms of foldl

For example: Erlang Programming

  1. Lists - chapter 2
  2. Recursion - chapter 3
  3. foldl, map chapter 9

Granted starting with recursion can be a bit rough but once the connections between

recursion -> foldl -> map

are wired in your brain certain things just make more sense.

It certainly is initially easier to treat reduce and map as separate concepts but I think understanding of “the whole” suffers.

From that traditional point of view, beginner exercises should focus on solving problems with recursion.

With that in mind:

defmodule Demo do
  def aggregate(list),
    do: aggregate(list, %{})

  defp aggregate([], acc) do
    acc
  end

  defp aggregate([[date, meridiem, _id, qty] | tail], acc) do
    new_acc =
      update_in(
        acc,
        [
          Access.key(date, %{}),
          Access.key(meridiem, 0)
        ],
        &(&1 + qty)
      )

    aggregate(tail, new_acc)
  end
end

# Note: list is an inappropriate data type
# for the data point - it should be a tuple
#
# tuple: fixed number of values of potentially dissimilar type
# list: variable number of values of usually similar type
# 
data_points = [
  ["2018-12-01", "AM", "ID123", 5000],
  ["2018-12-01", "AM", "ID545", 7000],
  ["2018-12-01", "PM", "ID545", 3000],
  ["2018-12-02", "AM", "ID545", 7000]
]

IO.puts("#{inspect(Demo.aggregate(data_points))}")
$ elixir demo.exs
%{"2018-12-01" => %{"AM" => 12000, "PM" => 3000}, "2018-12-02" => %{"AM" => 7000}}
$
3 Likes