Qqwy/elixir-map_diff

Way to find out changed value or potentially a bug!

Closed this issue · 4 comments

iex(4)> MapDiff.diff(%{a: 1, b: 2, c: %{ d: 5, e: 6 }}, %{a: 4, b: 2, c: %{d: 9, e: 6}})

%{  added: %{a: 4, c: %{d: 9, e: 6}},
  changed: :map_change,
  removed: %{a: 1, c: %{d: 5, e: 6}},  value: %{    a: %{added: 4, changed: :primitive_change, removed: 1},
    b: %{changed: :equal, value: 2},    c: %{
      added: %{d: 9},      changed: :map_change,      removed: %{d: 5},
      value: %{        d: %{added: 9, changed: :primitive_change, removed: 5},        e: %{changed: :equal, value: 6}
      }    }  }
}iex(5)> MapDiff.diff(%{a: 1, b: 2, c: %{ d: 5, e: 6 }}, %{a: 4, b: 2, c: %{d: 9, e: 6}}) |> Map.get(:added)%{a: 4, c: %{d: 9, e: 6}}
iex(6)> 

Since e: 6 is unchanged, is there any way to get only changed values easily ?
I thought :added does that. But maybe i'm misunderstanding something.

Qqwy commented

The way the library works on your example input is correct.
However, depending on what you are using MapDiff for, you might like to perform some post-processing.

A change might be one of two things:

  • changed: :primitive_change. In this case it is a simple value that was changed. You can see the previous and current value using the :added and :removed fields in that case.
  • changed: :map_change. In this case the change is a recursive change where one or multiple fields of a nested map are changed. Here, :value can be used to recurse deeper.

To for instance only get a list of (deeply nested) keys, consider the following recursive function:

defmodule Changes do
  def run(map_diff_output = %{changed: :map_change}) do
    map_diff_output[:value]
    |> Enum.flat_map(fn {key, val} ->
      # Prefix all nested changes with the key they appear under in a map
      run(val)
      |> Enum.map(fn nested_key -> [key | nested_key] end)
    end)
  end
  # A primitive change ends up in our list iff it was nested under a key
  def run(map_diff_output = %{changed: :primitive_change}) do
    [[]]
  end
  # Things that are equal are discarded:
  def run(_), do: []
end

If you want to get a map with only the (deeply nested) changed fields in there, you can do so as follows:

defmodule NestedChanges do
  def run(map_diff_output) do
    {:ok, res} = do_run(map_diff_output)
    res
  end

  defp do_run(map_diff_output = %{changed: :primitive_change}) do
    {:ok, map_diff_output[:added]}
  end
  defp do_run(map_diff_output = %{changed: :map_change}) do
    IO.inspect(map_diff_output)
    res =
      map_diff_output[:value]
      |> Enum.map(fn {key, val} -> {key, do_run(val)} end) # Recur on all keys
      |> Enum.reject(fn {key, val} -> val == :error end) # Remove all keys that do not contain changes
      |> Enum.map(fn {key, {:ok, val}} -> {key, val} end)
      |> Enum.into(%{})
    {:ok, res}
  end
  defp do_run(_), do: :error
end

There might be cleaner ways of writing these snippets, (it's a bit late here right now 😴 ) but hopefully you get the general idea.


What are you trying to do, exactly? What do you need the output of MapDiff for?
Maybe it makes sense to add some common ways of transforming the output of MapDiff like these two examples above to the library itself, if it turns out that they are useful in a variety of contexts.

I've a very big nested query (graphql) that returns a map. I need to return only the diff of those two maps. And then i send that diff to user via mail to indicate the changes he has made.

NestedChanges is breaking when two maps are equal.
Maybe It should return empty map or empty list.

defmodule Utils.MapDiff.NestedMapDiff do
  @moduledoc """
  NestedMapDiff
  """
  def run(map_diff_output) do
    {:ok, res} = do_run(map_diff_output)
    res
  end

  defp do_run(_map_diff_output = %{changed: :equal}) do
    {:ok, %{}}
  end

  defp do_run(map_diff_output = %{changed: :primitive_change}) do
    {:ok, map_diff_output[:added]}
  end

  defp do_run(map_diff_output = %{changed: :map_change}) do
    IO.inspect(map_diff_output)

    res =
      map_diff_output[:value]
      # Recur on all keys
      |> Enum.map(fn {key, val} -> {key, do_run(val)} end)
      # Remove all keys that do not contain changes
      |> Enum.reject(fn {_key, val} -> val == :error end)
      |> Enum.map(fn {key, {:ok, val}} -> {key, val} end)
      |> Enum.into(%{})

    {:ok, res}
  end

  defp do_run(_), do: :error
end

Ops..the better way is to use your NestedMapDiff and before that need to check Map.equal?(a,b). That did what i wanted.

Thanks @Qqwy I'm closing this issue as i found the way to do what i want from your comment.