Looping through a list of maps

Hello,

I have a list of maps as shown below:

[
  %{
    amount: #Decimal<0.10>,
    charge: #Decimal<0.90>,
    entity_name: "Entity 1",
    image_file: "81f4c6ba494a543caa942ceb6184b7ed.jpg",
    product_div: "Huawei Nova",
    product_sub_div: "Nova lite",
    product_sub_div_id: 398,
    reference: "Nova lite",
    req_date: "2019-10-11 13:24:23",
    status: "Failure",
    status_msg: "FAILED",
  },
  %{ 
    amount: #Decimal<0.10>,
    charge: #Decimal<0.90>,
    entity_name: "Entity 2",
    image_file: "26967a75af75ee2326adc1336d9b704e.jpg",
    product_div: "Grill Microwave",
    product_sub_div: "Binatone Microwave & Grill with Digital Display 20L 2017",
    product_sub_div_id: 411,
    reference: "Binatone Microwave & Grill with Digital Display 20L 2017",
    req_date: "2019-10-10 17:14:33",
    status: "Failure",
    status_msg: "FAILED",
  },
  %{
    amount: #Decimal<0.10>,
    charge: #Decimal<0.90>,
    entity_name: "Entity 2",
    image_file: "26967a75af75ee2326adc1336d9b704e.jpg",
    product_div: "Grill Microwave",
    product_sub_div: "Binatone Microwave & Grill with Digital Display 20L 2017",
    product_sub_div_id: 411,
    reference: "Binatone Microwave & Grill with Digital Display 20L 2017",
    req_date: "2019-10-10 11:02:38",
    status: "Failure",
    status_msg: "FAILED",
  }
]

I want to be able to loop through this list to achieve the result below:

[
  %{
    details: [
      %{
        amount: #Decimal<0.10>,
        charge: #Decimal<0.90>,
        reference: "Binatone Microwave & Grill with Digital Display 20L 2017",
        req_date: "2019-10-10 11:02:38",
        status: "Failure",
        status_msg: "FAILED"
      },
      %{
        amount: #Decimal<0.10>,
        charge: #Decimal<0.90>,
        reference: "Binatone Microwave & Grill with Digital Display 20L 2017",
        req_date: "2019-10-10 17:14:33",
        status: "Failure",
        status_msg: "FAILED"
      }
    ],
    entity_name: "Entity 2",
    image_file: "26967a75af75ee2326adc1336d9b704e.jpg",
    product_sub_div: "Binatone Microwave & Grill with Digital Display 20L 2017",
    product_sub_div_id: 411
  },
  %{
    details: [
      %{
        amount: #Decimal<0.10>,
        charge: #Decimal<0.90>,
        reference: "Nova lite",
        req_date: "2019-10-11 13:24:23",
        status: "Failure",
        status_msg: "FAILED"
      }
    ],
    entity_name: "Entity 1",
    image_file: "81f4c6ba494a543caa942ceb6184b7ed.jpg",
    product_sub_div: "Nova lite",
    product_sub_div_id: 398
  }
]

My code is as below:

def group_products_by_sub_div([], _id_arr, acc) do
        if length(acc) > 0 do
            {:ok, acc}
        else
            IO.puts "No record was retrieved"
        end
    end
    
    
    def group_products_by_sub_div([hd|tl], id_arr, acc) do
        product_sub_div_id=hd[:product_sub_div_id]
        product_sub_div=hd[:product_sub_div]
        amount=hd[:amount]
        status=hd[:status]
        reference=hd[:reference]
        req_date=hd[:req_date]
        status_msg=hd[:status_msg]
        entity_name=hd[:entity_name]
        image_file=hd[:image_file]
        charge=hd[:charge]
        
        main_map=%{product_sub_div: product_sub_div, product_sub_div_id: product_sub_div_id, entity_name: entity_name, image_file: image_file}
        details=[%{amount: amount, status: status, reference: reference, req_date: req_date, status_msg: status_msg, charge: charge}]
        
        cnt=Enum.count(id_arr, &(&1 == product_sub_div_id))
        id_arr=if cnt==1 do
            id_arr
        else
            [product_sub_div_id] ++ id_arr
        end
        str=if cnt==1 do
            map=Enum.find(acc, fn map -> map[:product_sub_div_id]==product_sub_div_id end)
            
            val=if !is_nil(map[:product_sub_div_id]) do
                details ++ map[:details]
            end
            
            Map.merge(main_map, %{details: val})
        else
            Map.merge(main_map, %{details: details})
        end
        
        acc=[str] ++ acc
        
        group_products_by_sub_div(tl, id_arr, acc)
    end

The above code gives me the result below, which is not accurate:

[
  %{
    details: [
      %{
        amount: #Decimal<0.10>,
        charge: #Decimal<0.90>,
        reference: "Binatone Microwave & Grill with Digital Display 20L 2017",
        req_date: "2019-10-10 11:02:38",
        status: "Failure",
        status_msg: "FAILED"
      },
      %{
        amount: #Decimal<0.10>,
        charge: #Decimal<0.90>,
        reference: "Binatone Microwave & Grill with Digital Display 20L 2017",
        req_date: "2019-10-10 17:14:33",
        status: "Failure",
        status_msg: "FAILED"
      }
    ],
    entity_name: "Entity 2",
    image_file: "26967a75af75ee2326adc1336d9b704e.jpg",
    product_sub_div: "Binatone Microwave & Grill with Digital Display 20L 2017",
    product_sub_div_id: 411
  },
  %{
    details: [
      %{
        amount: #Decimal<0.10>,
        charge: #Decimal<0.90>,
        reference: "Binatone Microwave & Grill with Digital Display 20L 2017",
        req_date: "2019-10-10 17:14:33",
        status: "Failure",
        status_msg: "FAILED"
      }
    ],
    entity_name: "Entity 2",
    image_file: "26967a75af75ee2326adc1336d9b704e.jpg",
    product_sub_div: "Binatone Microwave & Grill with Digital Display 20L 2017",
    product_sub_div_id: 411
  },
  %{
    details: [
      %{
        amount: #Decimal<0.10>,
        charge: #Decimal<0.90>,
        reference: "Nova lite",
        req_date: "2019-10-11 13:24:23",
        status: "Failure",
        status_msg: "FAILED"
      }
    ],
    entity_name: "Telefonika",
    image_file: "81f4c6ba494a543caa942ceb6184b7ed.jpg",
    product_sub_div: "Nova lite",
    product_sub_div_id: 398
  }
]

I would be grateful for any assistance in this regard.

Thanks.

Jerry

If this list may be long then maybe try to use other methods like bigger database query. If list will not be large then you can do it really simple like:

defmodule Example do
  @details_keys [:amount, :charge, :reference, :req_date, :status, :status_msg]
  @uniq_keys [:entity_name, :image_file, :product_sub_div, :product_sub_div_id]

  def sample(list) when is_list(list) do
    list
    |> Enum.group_by(&Map.take(&1, @uniq_keys), &Map.take(&1, @details_keys))
    |> Enum.map(&Map.put_new(elem(&1, 0), :details, elem(&1, 1)))
  end
end

What we are doing here is simple iteration of full list and grouping it by map of keys which should have same values. Extra third argument allows us to map grouped (by our map as key) values. Here we are simply taking another map, but this time from values which may be different.

In result you will have temporary map with:

  1. keys which stores map of unique values
  2. values which stores a list of details

Second thing is to iterate over such temporary map and simply put every value (list of maps) into a new map key called :details.

Helpful resources:

  1. Enum.group_by/3
  2. Enum.map/2
  3. Map.put_new/3
  4. Map.take/2
  5. elem/2
  6. is_list/1
6 Likes

As always @Eiji’s code is wonderfully terse and to the point.

My first pass would look more something like this:

defmodule Demo do
  defp group_entities(e, m),
    do: Map.update(m, e.entity_name, [e], &[e | &1])

  def extract_entity({_, [e | _] = list}) do
    e
    |> extract_props()
    |> Map.put(:details, extract_entity_details(list))
  end

  def extract_props(e),
    do: Map.take(e, [:entity_name, :image_file, :product_sub_div, :product_sub_div_id])

  def extract_entity_details(list),
    do: List.foldl(list, [], &collect_details/2)

  def collect_details(e, tail),
    do: [Map.take(e, [:amount, :charge, :reference, :req_date, :status, :status_msg]) | tail]

  def run(data) do
    data
    |> List.foldl(%{}, &group_entities/2)
    |> Enum.map(&extract_entity/1)
  end
end

data = [
  %{
    amount: 0.10,
    charge: 0.90,
    entity_name: "Entity 1",
    image_file: "81f4c6ba494a543caa942ceb6184b7ed.jpg",
    product_div: "Huawei Nova",
    product_sub_div: "Nova lite",
    product_sub_div_id: 398,
    reference: "Nova lite",
    req_date: "2019-10-11 13:24:23",
    status: "Failure",
    status_msg: "FAILED"
  },
  %{
    amount: 0.10,
    charge: 0.90,
    entity_name: "Entity 2",
    image_file: "26967a75af75ee2326adc1336d9b704e.jpg",
    product_div: "Grill Microwave",
    product_sub_div: "Binatone Microwave & Grill with Digital Display 20L 2017",
    product_sub_div_id: 411,
    reference: "Binatone Microwave & Grill with Digital Display 20L 2017",
    req_date: "2019-10-10 17:14:33",
    status: "Failure",
    status_msg: "FAILED"
  },
  %{
    amount: 0.10,
    charge: 0.90,
    entity_name: "Entity 2",
    image_file: "26967a75af75ee2326adc1336d9b704e.jpg",
    product_div: "Grill Microwave",
    product_sub_div: "Binatone Microwave & Grill with Digital Display 20L 2017",
    product_sub_div_id: 411,
    reference: "Binatone Microwave & Grill with Digital Display 20L 2017",
    req_date: "2019-10-10 11:02:38",
    status: "Failure",
    status_msg: "FAILED"
  }
]

IO.inspect(Demo.run(data))
$ elixir demo.exs
[
  %{
    details: [
      %{
        amount: 0.1,
        charge: 0.9,
        reference: "Nova lite",
        req_date: "2019-10-11 13:24:23",
        status: "Failure",
        status_msg: "FAILED"
      }
    ],
    entity_name: "Entity 1",
    image_file: "81f4c6ba494a543caa942ceb6184b7ed.jpg",
    product_sub_div: "Nova lite",
    product_sub_div_id: 398
  },
  %{
    details: [
      %{
        amount: 0.1,
        charge: 0.9,
        reference: "Binatone Microwave & Grill with Digital Display 20L 2017",
        req_date: "2019-10-10 17:14:33",
        status: "Failure",
        status_msg: "FAILED"
      },
      %{
        amount: 0.1,
        charge: 0.9,
        reference: "Binatone Microwave & Grill with Digital Display 20L 2017",
        req_date: "2019-10-10 11:02:38",
        status: "Failure",
        status_msg: "FAILED"
      }
    ],
    entity_name: "Entity 2",
    image_file: "26967a75af75ee2326adc1336d9b704e.jpg",
    product_sub_div: "Binatone Microwave & Grill with Digital Display 20L 2017",
    product_sub_div_id: 411
  }
]
3 Likes

Many thanks, @Eiji and @peerreynders.

I am trying out the solutions given, please.

I will share my response shortly.

I am grateful.

Kind regards,

Jerry

Solution works perfectly.

Still trying to understand the few lines of code used to achieve this.

Thanks a lot.

Jerry

One possible variation:

  @prop_names [:entity_name, :image_file, :product_div, :product_sub_div, :product_sub_div_id]

  defp extract_props(e),
    do: Map.take(e, @prop_names)

  def collect_details(e, tail),
    do: [Map.drop(e, @prop_names) | tail]

Still trying to understand the few lines of code used to achieve this.

My personal preference would have been:

defmodule Demo do
  @prop_names [:entity_name, :image_file, :product_sub_div, :product_sub_div_id]
  defp entity_props(e),
    do: Map.take(e, @prop_names)

  @detail_names [:amount, :charge, :reference, :req_date, :status, :status_msg]
  defp entity_details(e),
    do: Map.take(e, @detail_names)

  defp make_entity({props, details}),
    do: Map.put_new(props, :details, details)

  def run(list) when is_list(list) do
    list
    |> Enum.group_by(&entity_props/1, &entity_details/1)
    |> Enum.map(&make_entity/1)
  end
end

I believe it makes the code more “scannable” - i.e. my brain only has to recognize one standard function per line and pick up on the concepts expressed by the names of the custom functions rather than continually mentally parse each line of code which tends to be fatiguing.

Thanks @peerreynders.