How to get a map with its key being a tuple matching the count of all characters and its value being a list of anagrams?

Basically, what I’m trying to do is that I want a map where the key is a tuple containing count of all same characters in the list and its values being the list of anagrams. To explain it in more clearer terms, I want the following:

%{{0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0} => ['eggs', 'gegs'], {1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0} => ['clay', 'alcy']}

Now, currently what I am getting is the following:

%{ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0} => "y", {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0} => "s", {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} => "l", {0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} => "g", {0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} => "e", {0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} => "c", {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} => "a" }

I basically got this by writing the following code:

defmodule Anagram do
  def traverse(strings, acc \\ Map.new()) do
    Enum.reduce(strings, acc, fn string, acc ->
      string
      |> String.graphemes()
      |> Enum.reduce(acc, &Anagram.fun/2)
    end)
  end

  def fun(char, acc) do
    {index, _} = :binary.match("abcdefghijklmnopqrstuvwxyz", char)
    count = List.duplicate(0, 26)
    count = List.update_at(count, index, &(&1 + 1))
    tupcount = List.to_tuple(count)
    Map.update(acc, tupcount, count, fn _ -> char end)
  end
end

IO.inspect Anagram.traverse ["eggs", "gegs", "clay", "alcy"]

Any idea what needs to be corrected here? I’m pretty new to Elixir and experimenting with different stuff but I’m a little lost over here. Any help will be appreciated.

You could map over the graphemes to get a list of matching indexes and then reduce it to the tuple like this:

def fun(string) do
    string
    |> String.graphemes()
    |> Enum.map(fn char ->
      {index, _} = :binary.match("abcdefghijklmnopqrstuvwxyz", char)
      index
    end)
    |> Enum.reduce(List.duplicate(0, 26), fn index, list ->
      List.update_at(list, index, &(&1 + 1))
    end)
    |> List.to_tuple()
end

After then you can reduce it to a map:

def traverse(strings) do
    strings
    |> Enum.reduce(%{}, fn string, map ->
      key = fun(string)
      Map.update(map, key, [string], fn list -> list ++ [string] end)
    end)
  end
3 Likes

I took a pass at this code and extracted the key generation into a calculate_key function. Then moved map updates into the traverse function body.

defmodule Anagram do
  def traverse(strings, acc \\ Map.new()) do
    Enum.reduce(strings, acc, fn string, acc ->
      # Update the map for each string, appending to the existing list
      # if there is already a word with a matching key.
      Map.update(acc, calculate_key(string), [string], &[string | &1])
    end)
  end

  # Create a tuple from the character counts of a given word
  def calculate_key(word) do
    word
    |> String.graphemes()
    |> Enum.reduce(List.duplicate(0, 26), fn char, acc ->
      {index, _} = :binary.match("abcdefghijklmnopqrstuvwxyz", char)

      List.update_at(acc, index, &(&1 + 1))
    end)
    |> List.to_tuple()
  end
end

IO.inspect(Anagram.traverse(["eggs", "gegs", "clay", "alcy"]))

Edit: looks like @moogle19 beat me to the implementation—glad to see that someone else would approach the problem in a similar way!

2 Likes