Assign members of a list into two lists

I have an input that is a list of players:

  [
  %{rating: 8, member_id: 1},
  %{rating: 5, member_id: 4},
  %{rating: 6, member_id: 2},
  %{rating: 7, member_id: 3}
  ]

I then want to divide them into two teams that would look like this:

[%{team_id: 0, members: []}, %{team_id: 1, members: []}]

The team with the least amount of players picks first; if there is a tie then the team with the lowest summed rating of members picks. The team will always pick the first in the player list that hasn’t already been picked.

So team 1 would pick member id 1, then team 2 would pick member id 4, then team 2 would pick member id 2, then team 1 would pick member id 3.

My first thought is to use Map.reduce with

[%{team_id: 0, members: []}, %{team_id: 1, members: []}]

as the accumulator. But not sure how to update it.

You can do in multiple ways:

Plain pattern matching

This works only on fixed length lists, technically it can be applied for longer lists, but that requires some additional steps.

[a0, a1, b0, b1] = list

[%{team_id: 0, members: [a0, b0]}, %{team_id: 1, members: [a1, b1]}]

Using Enum.reduce/3

{team0, team1} = Enum.reduce(list, {[], []}, fn member, {curr, next} ->
  {next, [member | curr]}
end)

[%{team_id: 0, members: team0}, %{team_id: 1, members: team1}]

Though there team0 and team1 members will be in reverse order. You can call Enum.reverse/1 on these to change that.

Enum.group_by/3

list
|> Enum.with_index()
|> Enum.group_by(fn {x, _} -> rem(x, 2) end, fn {_, team} -> team end)
|> Enum.map(fn {id, members} -> %{team_id: id, members: members} end)

There probably are some more ways to do so, but it is getting late there and I do not want to think too much about these.

You’d need at least one other piece of “state” in the accumulator of reduce - which team is picking (0 or 1).

HOWEVER

reduce is powerful, but also complicated. Using more-specific functions from Enum may be more readable. For instance:

input = [
  %{rating: 8, member_id: 1},
  %{rating: 5, member_id: 4},
  %{rating: 6, member_id: 2},
  %{rating: 7, member_id: 3}
]

{members0, members1} =
  input
  |> Enum.chunk_every(2) # produces lists with 2 players, except if there's leftover at the end
  |> Enum.map(fn
    [p0, p1] -> {p0, p1}
    [p0] -> {p0, nil}
  end)
  |> Enum.unzip()

[%{team_id: 0, members: members0}, %{team_id: 1, members: members1}]

I ended up writing some code

  @doc """
  member_list: A sorted list of members e.g.
  [
  %{rating: 8, member_id: 1},
  %{rating: 5, member_id: 4},
  %{rating: 6, member_id: 2},
  %{rating: 7, member_id: 3}
  ]
  """
  def assign_teams(member_list) do
    Enum.reduce(member_list, [%{team_id: 0, members: []}, %{team_id: 1, members: []}], fn x,
                                                                                          acc ->
      picking_team = get_picking_team(acc)
      update_picking_team = Map.merge(picking_team, %{members: [x | picking_team.members]})
      [update_picking_team | get_non_picking_teams(acc, picking_team)]
    end)
  end

  def get_picking_team(teams) do
    default_picking_team = Enum.at(teams, 0)

    Enum.reduce(teams, default_picking_team, fn x, acc ->
      # Team is picker if it has least members
      if(length(x.members) < length(acc.members)) do
        x
      else
        # Team is picker if it is tied for least and has lower team rating
        if(
          length(x.members) == length(acc.members) && get_team_rating(x) < get_team_rating(acc)
        ) do
          x
        else
          acc
        end
      end
    end)
  end

  def get_non_picking_teams(teams, picking_team) do
    Enum.filter(teams, fn x -> x.team_id != picking_team.team_id end)
  end

  def get_team_rating(team) do
    Enum.reduce(team.members, 0, fn x, acc ->
      acc + x.rating
    end)
  end

Feel free to suggest any improvements.

Cache length/1 if you need to use it repeatedly, because that function is O(n)

1 Like