Pattern Matching Aganist a Nested List of Maps

defmodule Sandbox do

def run do 
fast_cars = 
%{
  "Tesla" => [
    %{
      make: "Tesla",
      metrics: [
        %{
          maker: "Tesla",
          SPC: 0.00682111,
          competitor: "Aston_Martin",
          DOM: 1028.1163347634
        },
        %{
          maker: "Tesla",
          SPC: 0.017515,
          competitor: "Porsche",
          DOM: 434.021755511914
        }
      ]
    }
  ],
  "Porsche" => [
    %{
      make: "Porsche",
      metrics: [
        %{
          maker: "Porsche",
          SPC: 1.008,
          competitor: "Maserati",
          DOM: 52793.9857518164
        }
      ]
    }
  ],
  "Ferrari" => [
    %{
      make: "Ferrari",
      metrics: [
        %{
          maker: "Ferrari",
          SPC: 352.669656,
          competitor: "Porsche",
          DOM: 249687.2418829
        },
        %{
          maker: "Ferrari",
          SPC: 355.204959,
          competitor: "Maserati",
          DOM: 153819.425075646
        },
        %{
          maker: "Ferrari",
          SPC: 136.52285808,
          competitor: "Aston_Martin",
          DOM: 68129.7323887293
        },
        %{
          maker: "Ferrari",
          SPC: 0.0332048,
          competitor: "Lamborghini",
          DOM: 20191.7191403122
        }
      ]
    }
  ],
  "Bentley" => [
    %{
      make: "Bentley",
      metrics: [
        %{
          maker: "Bentley",
          SPC: 0.04822,
          competitor: "Aston_Martin",
          DOM: 2353.90735023826
        }
      ]
    }
  ],
  "Bugatti" => [
    %{
      make: "Bugatti",
      metrics: [
        %{
          maker: "Bugatti",
          SPC: 70.53574767,
          competitor: "Aston_Martin",
          DOM: 518.045106061978
        },
        %{
          maker: "Bugatti",
          SPC: 0.01636395,
          competitor: "Lamborghini",
          DOM: 320.366445388147
        }
      ]
    }
  ],
  "BMW" => [
    %{
      make: "BMW",
      metrics: [
        %{
          maker: "BMW",
          SPC: 9.7388e-4,
          competitor: "Aston_Martin",
          DOM: 511.4216487588
        }
      ]
    }
  ],
  "Lamborghini" => [
    %{
      make: "Lamborghini",
      metrics: [
        %{
          maker: "Lamborghini",
          SPC: 10667.97,
          competitor: "Maserati",
          DOM: 33680.6277176563
        }, 
        %{
          maker: "Lamborghini",
          SPC: 10556.305993,
          competitor: "Porsche",
          DOM: 19869.4635589822
        }
      ]
    }
  ],
  "Jaguar" => [
    %{
      make: "Jaguar",
      metrics: [
        %{
          maker: "Jaguar",
          SPC: 19.999,
          competitor: "Aston_Martin",
          DOM: 3014.45913570669
        }
      ]
    }
  ],
  "Koenigsegg" => [
    %{
      make: "Koenigsegg",
      metrics: [
        %{
          maker: "Koenigsegg",
          SPC: 64.181613,
          competitor: "Porsche",
          DOM: 9829.77045465923
        },
        %{
          maker: "Koenigsegg",
          SPC: 0.0060591,
          competitor: "Lamborghini",
          DOM: 2548.17000680984
        },
        %{
          maker: "Koenigsegg",
          SPC: 24.82987174,
          competitor: "Aston_Martin",
          DOM: 495.244803630221
        }
      ]
    }
  ],
  "Maserati" => [
    %{
      make: "Maserati",
      metrics: [
        %{
          maker: "Maserati",
          SPC: 28.038444,
          competitor: "Lotus",
          DOM: 313.655750686137
        }
      ]
    }
  ],
  "McLaren" => [
    %{
      make: "McLaren",
      metrics: [
        %{
          maker: "McLaren",
          SPC: 0.0749,
          competitor: "Aston_Martin",
          DOM: 291.0407460638
        }
      ]
    }
  ],
  "Pagani" => [
    %{
      make: "Pagani",
      metrics: [
        %{
          maker: "Pagani",
          SPC: 0.03297,
          competitor: "Aston_Martin",
          DOM: 570.362211630001
        }
      ]
    }
  ]
}

IsolateUnpairedMakerGroups.run(fast_cars)

end
defmodule IsolateUnpairedMakerGroups do
  def run(enumerable) do
    Enum.map(enumerable, &isolate_unilateral_item/1)
  end

  def isolate_unilateral_item(
  {
  _main_key,
    [
      %{
        _key_two: _value,
        _key_three: [_] = list
      }
    ]
}
    )
  do
    list
  end

  def isolate_unilateral_item(_), do: [:nogo]
end
end

The desired output from IsolateUnpairedMakerGroups.run(fast_cars) is a list of maps only of makers with a single competitor:

[
 %{
          maker: "Porsche",
          SPC: 1.008,
          competitor: "Maserati",
          DOM: 52793.9857518164
        }, 
 %{
          maker: "Bentley",
          SPC: 0.04822,
          competitor: "Aston_Martin",
          DOM: 2353.90735023826
        }, 
%{
          maker: "BMW",
          SPC: 9.7388e-4,
          competitor: "Aston_Martin",
          DOM: 511.4216487588
        }, 
 %{
          maker: "Jaguar",
          SPC: 19.999,
          competitor: "Aston_Martin",
          DOM: 3014.45913570669
        }, 
%{
          maker: "Maserati",
          SPC: 28.038444,
          competitor: "Lotus",
          DOM: 313.655750686137
        }, 
%{
          maker: "McLaren",
          SPC: 0.0749,
          competitor: "Aston_Martin",
          DOM: 291.0407460638
        }, 
 %{
          maker: "Pagani",
          SPC: 0.03297,
          competitor: "Aston_Martin",
          DOM: 570.362211630001
        }
]

Instead, this is what we get:

iex(30)> Sandbox.run
[
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo],
  [:nogo]
]

How should the code be refactored so IsolateUnpairedMakerGroups.run(fast_cars) works as intended?

I don’t think you can do this with pattern matching, and even if you could it would be hard to follow. Your data structure is a little weird, but here’s a solution:

  def run(enumerable) do
    enumerable
      |> competitors_per_make()
      |> makes_with_single_competitor()
  end

  # Gets unique competitors per make, returns list of maps
  # with shape %{make: make, competitors: []}
  def competitors_per_make(list) do
    Enum.map(list, fn {make, weird_list} ->
      first = hd(weird_list)
      competitors =
        first.metrics
        |> Enum.map(&(&1.competitor))
        |> Enum.uniq()

      %{make: make, competitors: competitors}
    end)
  end

  def makes_with_single_competitor(list) do
    Enum.filter(list, &(length(&1.competitors) == 1))
  end

Edit: results if you run the above:

[
  %{competitors: ["Aston_Martin"], make: "BMW"},
  %{competitors: ["Aston_Martin"], make: "Bentley"},
  %{competitors: ["Aston_Martin"], make: "Jaguar"},
  %{competitors: ["Lotus"], make: "Maserati"},
  %{competitors: ["Aston_Martin"], make: "McLaren"},
  %{competitors: ["Aston_Martin"], make: "Pagani"},
  %{competitors: ["Maserati"], make: "Porsche"}
]
4 Likes

It is never going to match your pattern…

%{
        _key_two: _value,
        _key_three: [_] = list
      }

You cannot treat atom keys as throw away keys, this only works for variables…

You could see what You really get by changing this…

to

def isolate_unilateral_item(wrong_match), do: IO.inspect(wrong_match)

Maybe like this…

fast_cars 
|> Enum.flat_map(fn {_k, l} -> Enum.flat_map(l, & &1.metrics) end) 
|> Enum.group_by(& &1.maker) 
|> Enum.filter(fn {_k, l} -> length(l) == 1 end) 
|> Enum.flat_map(& elem(&1, 1))
4 Likes

Perfect, thanks so much! :slight_smile:

1 Like

Thanks for your insights :slight_smile:

Here goes a small tweak with sort and [head | tail] recursion:

defmodule Example do
  def sample(map) when is_map(map) do
    map
    |> Enum.flat_map(fn {_, list} -> Enum.flat_map(list, & &1.metrics) end)
    |> Enum.sort_by(& &1.maker)
    |> sample([], nil)
  end

  # when done return acc
  def sample([], acc, _), do: acc
  # skip when previous maker (sorted list) matches current
  def sample([%{maker: maker} | tail], acc, maker), do: sample(tail, acc, maker)
  # else skip when current maker (sorted list) matches next
  def sample([%{maker: maker}, %{maker: maker} | tail], acc, _), do: sample(tail, acc, maker)
  # otherwise add item to acc and continue recursion
  def sample([item | tail], acc, _), do: sample(tail, [item | acc], nil)
end

Benchmark

script
# example.exs

defmodule Example do
  def group_by(map) when is_map(map) do
    map
    |> Enum.flat_map(fn {_k, l} -> Enum.flat_map(l, & &1.metrics) end)
    |> Enum.group_by(& &1.maker)
    |> Enum.filter(fn {_k, l} -> length(l) == 1 end)
    |> Enum.flat_map(&elem(&1, 1))
  end

  def sort_by(map) when is_map(map) do
    map
    |> Enum.flat_map(fn {_, list} -> Enum.flat_map(list, & &1.metrics) end)
    |> Enum.sort_by(& &1.maker)
    |> recur([], nil)
  end

  defp recur([], acc, _), do: acc
  defp recur([%{maker: maker} | tail], acc, maker), do: recur(tail, acc, maker)
  defp recur([%{maker: maker}, %{maker: maker} | tail], acc, _), do: recur(tail, acc, maker)
  defp recur([item | tail], acc, _), do: recur(tail, [item | acc], nil)
end

fast_cars = %{
  "Tesla" => [
    %{
      make: "Tesla",
      metrics: [
        %{
          maker: "Tesla",
          SPC: 0.00682111,
          competitor: "Aston_Martin",
          DOM: 1028.1163347634
        },
        %{
          maker: "Tesla",
          SPC: 0.017515,
          competitor: "Porsche",
          DOM: 434.021755511914
        }
      ]
    }
  ],
  "Porsche" => [
    %{
      make: "Porsche",
      metrics: [
        %{
          maker: "Porsche",
          SPC: 1.008,
          competitor: "Maserati",
          DOM: 52793.9857518164
        }
      ]
    }
  ],
  "Ferrari" => [
    %{
      make: "Ferrari",
      metrics: [
        %{
          maker: "Ferrari",
          SPC: 352.669656,
          competitor: "Porsche",
          DOM: 249_687.2418829
        },
        %{
          maker: "Ferrari",
          SPC: 355.204959,
          competitor: "Maserati",
          DOM: 153_819.425075646
        },
        %{
          maker: "Ferrari",
          SPC: 136.52285808,
          competitor: "Aston_Martin",
          DOM: 68129.7323887293
        },
        %{
          maker: "Ferrari",
          SPC: 0.0332048,
          competitor: "Lamborghini",
          DOM: 20191.7191403122
        }
      ]
    }
  ],
  "Bentley" => [
    %{
      make: "Bentley",
      metrics: [
        %{
          maker: "Bentley",
          SPC: 0.04822,
          competitor: "Aston_Martin",
          DOM: 2353.90735023826
        }
      ]
    }
  ],
  "Bugatti" => [
    %{
      make: "Bugatti",
      metrics: [
        %{
          maker: "Bugatti",
          SPC: 70.53574767,
          competitor: "Aston_Martin",
          DOM: 518.045106061978
        },
        %{
          maker: "Bugatti",
          SPC: 0.01636395,
          competitor: "Lamborghini",
          DOM: 320.366445388147
        }
      ]
    }
  ],
  "BMW" => [
    %{
      make: "BMW",
      metrics: [
        %{
          maker: "BMW",
          SPC: 9.7388e-4,
          competitor: "Aston_Martin",
          DOM: 511.4216487588
        }
      ]
    }
  ],
  "Lamborghini" => [
    %{
      make: "Lamborghini",
      metrics: [
        %{
          maker: "Lamborghini",
          SPC: 10667.97,
          competitor: "Maserati",
          DOM: 33680.6277176563
        },
        %{
          maker: "Lamborghini",
          SPC: 10556.305993,
          competitor: "Porsche",
          DOM: 19869.4635589822
        }
      ]
    }
  ],
  "Jaguar" => [
    %{
      make: "Jaguar",
      metrics: [
        %{
          maker: "Jaguar",
          SPC: 19.999,
          competitor: "Aston_Martin",
          DOM: 3014.45913570669
        }
      ]
    }
  ],
  "Koenigsegg" => [
    %{
      make: "Koenigsegg",
      metrics: [
        %{
          maker: "Koenigsegg",
          SPC: 64.181613,
          competitor: "Porsche",
          DOM: 9829.77045465923
        },
        %{
          maker: "Koenigsegg",
          SPC: 0.0060591,
          competitor: "Lamborghini",
          DOM: 2548.17000680984
        },
        %{
          maker: "Koenigsegg",
          SPC: 24.82987174,
          competitor: "Aston_Martin",
          DOM: 495.244803630221
        }
      ]
    }
  ],
  "Maserati" => [
    %{
      make: "Maserati",
      metrics: [
        %{
          maker: "Maserati",
          SPC: 28.038444,
          competitor: "Lotus",
          DOM: 313.655750686137
        }
      ]
    }
  ],
  "McLaren" => [
    %{
      make: "McLaren",
      metrics: [
        %{
          maker: "McLaren",
          SPC: 0.0749,
          competitor: "Aston_Martin",
          DOM: 291.0407460638
        }
      ]
    }
  ],
  "Pagani" => [
    %{
      make: "Pagani",
      metrics: [
        %{
          maker: "Pagani",
          SPC: 0.03297,
          competitor: "Aston_Martin",
          DOM: 570.362211630001
        }
      ]
    }
  ]
}

Benchee.run(%{
  "group_by" => fn -> Example.group_by(fast_cars) end,
  "sort_by" => fn -> Example.sort_by(fast_cars) end
})
results
Generated example app
Operating System: Linux
CPU Information: Intel(R) Core(TM) i7-3630QM CPU @ 2.40GHz
Number of Available Cores: 8
Available memory: 15.53 GB
Elixir 1.11.0-rc.0
Erlang 23.1

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 14 s

Benchmarking group_by...
Benchmarking sort_by...

Name               ips        average  deviation         median         99th %
sort_by       137.58 K        7.27 μs   ±365.63%        6.79 μs       15.95 μs
group_by      119.51 K        8.37 μs   ±364.74%        7.65 μs       18.84 μs

Comparison: 
sort_by       137.58 K
group_by      119.51 K - 1.15x slower +1.10 μs
2 Likes