diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 95a4794..ef7b1df 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -9,11 +9,18 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - otp: ['24.2', '25.0'] - elixir: ['1.12.3', '1.13.3', '1.14.0'] - exclude: - - otp: '25.0' - elixir: '1.12.3' + include: + - elixir: 1.14.5 + otp: 24.3 + + - elixir: 1.15.4 + otp: 25.3 + + - elixir: 1.16.3 + otp: 26.2 + + - otp: 27.2 + elixir: 1.18.1 steps: - uses: actions/checkout@v2 - uses: erlef/setup-beam@v1 diff --git a/.gitignore b/.gitignore index 735ed1d..a2eb72c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ erl_crash.dump # QuickCheck files /.eqc-info /*.eqc +.elixir_ls/ \ No newline at end of file diff --git a/lib/edge.ex b/lib/edge.ex index 4563ccc..7236374 100644 --- a/lib/edge.ex +++ b/lib/edge.ex @@ -8,17 +8,20 @@ defmodule Graph.Edge do defstruct v1: nil, v2: nil, weight: 1, - label: nil + label: nil, + properties: %{} @type t :: %__MODULE__{ v1: Graph.vertex(), v2: Graph.vertex(), weight: integer | float, - label: term + label: term, + properties: map } @type edge_opt :: {:weight, integer | float} | {:label, term} + | {:properties, map} @type edge_opts :: [edge_opt] @doc """ @@ -36,11 +39,15 @@ defmodule Graph.Edge do @spec new(Graph.vertex(), Graph.vertex()) :: t @spec new(Graph.vertex(), Graph.vertex(), [edge_opt]) :: t | no_return def new(v1, v2, opts \\ []) when is_list(opts) do + {weight, opts} = Keyword.pop(opts, :weight, 1) + {label, opts} = Keyword.pop(opts, :label) + %__MODULE__{ v1: v1, v2: v2, - weight: Keyword.get(opts, :weight, 1), - label: Keyword.get(opts, :label) + weight: weight, + label: label, + properties: Keyword.get(opts, :properties, %{}) } end @@ -48,12 +55,13 @@ defmodule Graph.Edge do def options_to_meta(opts) when is_list(opts) do label = Keyword.get(opts, :label) weight = Keyword.get(opts, :weight, 1) + properties = Keyword.get(opts, :properties, %{}) - case {label, weight} do - {_, w} = meta when is_number(w) -> - meta + case {label, %{weight: weight, properties: properties}} do + {label, %{weight: w} = meta} when is_number(w) -> + {label, meta} - {_, _} -> + _other -> raise ArgumentError, message: "invalid value for :weight, must be an integer" end end diff --git a/lib/graph.ex b/lib/graph.ex index b66670f..285689f 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -28,10 +28,13 @@ defmodule Graph do defstruct in_edges: %{}, out_edges: %{}, edges: %{}, + edge_index: %{}, vertex_labels: %{}, vertices: %{}, type: :directed, - vertex_identifier: &Graph.Utils.vertex_id/1 + vertex_identifier: &Graph.Utils.vertex_id/1, + partition_by: &Graph.Utils.by_edge_label/1, + multigraph: false alias Graph.{Edge, EdgeSpecificationError} @@ -43,17 +46,27 @@ defmodule Graph do @type label :: term @type edge_weight :: integer | float @type edge_key :: {vertex_id, vertex_id} - @type edge_value :: %{label => edge_weight} + # @type edge_value :: %{label => edge_weight} + @type edge_index_key :: label | term + @type edge_properties :: %{ + label: label, + weight: edge_weight, + properties: map + } + @type edge_value :: %{label => edge_properties()} @type graph_type :: :directed | :undirected @type vertices :: %{vertex_id => vertex} @type t :: %__MODULE__{ in_edges: %{vertex_id => MapSet.t()}, out_edges: %{vertex_id => MapSet.t()}, edges: %{edge_key => edge_value}, + edge_index: %{edge_index_key => MapSet.t()}, vertex_labels: %{vertex_id => term}, vertices: %{vertex_id => vertex}, type: graph_type, - vertex_identifier: (vertex() -> term()) + vertex_identifier: (vertex() -> term()), + partition_by: (Edge.t() -> list(edge_index_key)), + multigraph: boolean() } @type graph_info :: %{ :num_edges => non_neg_integer(), @@ -70,6 +83,19 @@ defmodule Graph do - `type: :directed | :undirected`, specifies what type of graph this is. Defaults to a `:directed` graph. - `vertex_identifier`: a function which accepts a vertex and returns a unique identifier of said vertex. Defaults to `Graph.Utils.vertex_id/1`, a hash of the whole vertex utilizing `:erlang.phash2/2`. + - `multigraph: true | false | fn edge -> key end`, enables edge indexing by a key. + - When `true`, the key is the edge label itself. + - When `false` no additional memory is used for sets of . + - `partition_by`: a function which accepts an `%Edge{}` and returns a list of unique identifiers used as the partition keys. + Defaults to `Graph.Utils.by_edge_label/1`, which partitions edges by the label when multigraphs are enabled. + + ### Multigraph Edge Indexing + + When `multigraph: true` is enabled the `partition_by` function maintains sets of edges for the partition. + This option enables a space for time trade-off for Map access retrieval partitioned edges of a kind i.e. [multigraph](https://en.wikipedia.org/wiki/Multigraph) capabilities. + + This edge adjacency index can be useful for graphs where many different kinds of edges exist between the same vertices and + iteration over all edges is prohibitive. ## Example @@ -87,11 +113,23 @@ defmodule Graph do iex> g = Graph.new(vertex_identifier: fn v -> :erlang.phash2(v) end) |> Graph.add_edges([{:a, :b}, {:b, :a}]) ...> Graph.edges(g) [%Graph.Edge{v1: :a, v2: :b}, %Graph.Edge{v1: :b, v2: :a}] + + iex> g = Graph.new(multigraph: true, partition_by: fn edge -> [edge.weight] end) |> Graph.add_edges([{:a, :b, weight: 1}, {:b, :a, weight: 2}]) + ...> Graph.edges(g, by: 1) + [%Graph.Edge{v1: :a, v2: :b, weight: 1}] """ def new(opts \\ []) do type = Keyword.get(opts, :type) || :directed vertex_identifier = Keyword.get(opts, :vertex_identifier) || (&Graph.Utils.vertex_id/1) - %__MODULE__{type: type, vertex_identifier: vertex_identifier} + partition_by = Keyword.get(opts, :partition_by) || (&Graph.Utils.by_edge_label/1) + multigraph = Keyword.get(opts, :multigraph, false) + + %__MODULE__{ + type: type, + vertex_identifier: vertex_identifier, + partition_by: partition_by, + multigraph: multigraph + } end @doc """ @@ -427,15 +465,20 @@ defmodule Graph do target = Map.get(vs, out_neighbor) meta = Map.get(meta, {source_id, out_neighbor}) - Enum.map(meta, fn {label, weight} -> - Edge.new(source, target, label: label, weight: weight) + Enum.map(meta, fn {label, %{weight: weight, properties: properties}} -> + Edge.new(source, target, label: label, weight: weight, properties: properties) end) end) end) end @doc """ - Returns a list of all edges inbound or outbound from vertex `v`. + Returns a list of all edges inbound or outbound from vertex `v` or by multigraph traversal options. + + ## Options when `multigraph: true` + + - `:where` - a function that accepts an edge and must return a boolean to include the edge. + - `:by` - a keyword list of partitions to traverse. If not provided, all edges are traversed. ## Example @@ -446,8 +489,32 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edges([{:a, :b}, {:b, :c}]) ...> Graph.edges(g, :d) [] + + iex> g = Graph.new(multigraph: true) |> Graph.add_edges([{:a, :b}, {:b, :c}]) + ...> g = Graph.add_edge(g, :a, :b, label: :contains) + ...> Graph.edges(g, :a, by: [:contains]) + [%Graph.Edge{v1: :a, v2: :b, label: :contains}] + + iex> g = Graph.new(multigraph: true) |> Graph.add_edges([{:a, :b}, {:b, :c}]) + ...> g = Graph.add_edge(g, :a, :b, label: :contains, weight: 2) + ...> Graph.edges(g, :a, where: fn edge -> edge.weight == 2 end) + [%Graph.Edge{v1: :a, v2: :b, label: :contains, weight: 2}] """ - @spec edges(t, vertex) :: [Edge.t()] + @spec edges(t, vertex | keyword()) :: [Edge.t()] + + def edges(%__MODULE__{multigraph: true} = g, opts) when is_list(opts) do + where_fun = opts[:where] + + if Keyword.has_key?(opts, :by) do + partitions = partition_for_opts(opts[:by]) + edges_in_partitions(g, partitions, where_fun) + else + g + |> edges() + |> filter_edges(where_fun) + end + end + def edges( %__MODULE__{ in_edges: ie, @@ -472,8 +539,12 @@ defmodule Graph do edge_meta when is_map(edge_meta) -> v2 = Map.get(vs, v2_id) - for {label, weight} <- edge_meta do - Edge.new(v2, v, label: label, weight: weight) + for {label, meta_value} <- edge_meta do + Edge.new(v2, v, + label: label, + weight: meta_value.weight, + properties: meta_value.properties + ) end end end) @@ -487,8 +558,12 @@ defmodule Graph do edge_meta when is_map(edge_meta) -> v2 = Map.get(vs, v2_id) - for {label, weight} <- edge_meta do - Edge.new(v, v2, label: label, weight: weight) + for {label, meta_value} <- edge_meta do + Edge.new(v, v2, + label: label, + weight: meta_value.weight, + properties: meta_value.properties + ) end end end) @@ -497,21 +572,54 @@ defmodule Graph do end @doc """ - Returns a list of all edges between `v1` and `v2`. + Returns a list of all edges between `v1` and `v2` or connected to `v1` given multigraph options. + + ## Options when `multigraph: true` + + - `:where` - a function that accepts an edge and must return a boolean to include the edge. + - `:by` - a single partition or list of partitions to traverse. If not provided, all edges are traversed. ## Example iex> g = Graph.new |> Graph.add_edge(:a, :b, label: :uses) ...> g = Graph.add_edge(g, :a, :b, label: :contains) ...> Graph.edges(g, :a, :b) - [%Graph.Edge{v1: :a, v2: :b, label: :contains}, %Graph.Edge{v1: :a, v2: :b, label: :uses}] + [%Graph.Edge{v1: :a, v2: :b, label: :uses}, %Graph.Edge{v1: :a, v2: :b, label: :contains}] iex> g = Graph.new(type: :undirected) |> Graph.add_edge(:a, :b, label: :uses) ...> g = Graph.add_edge(g, :a, :b, label: :contains) ...> Graph.edges(g, :a, :b) - [%Graph.Edge{v1: :a, v2: :b, label: :contains}, %Graph.Edge{v1: :a, v2: :b, label: :uses}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: :uses, properties: %{}}, %Graph.Edge{label: :contains, properties: %{}, v1: :a, v2: :b, weight: 1}] + + iex> g = Graph.new(multigraph: true) |> Graph.add_edges([{:a, :b}, {:b, :c}]) + ...> g = Graph.add_edge(g, :a, :b, label: :contains) + ...> Graph.edges(g, :a, by: :contains) + [%Graph.Edge{v1: :a, v2: :b, label: :contains}] + + iex> g = Graph.new(multigraph: true) |> Graph.add_edges([{:a, :b}, {:b, :c}]) + ...> g = Graph.add_edge(g, :a, :b, label: :contains, weight: 2) + ...> Graph.edges(g, :a, by: :contains, where: fn edge -> edge.weight == 2 end) + [%Graph.Edge{v1: :a, v2: :b, label: :contains, weight: 2}] """ - @spec edges(t, vertex, vertex) :: [Edge.t()] + @spec edges(t, vertex, vertex | keyword()) :: [Edge.t()] + def edges( + %__MODULE__{multigraph: true} = g, + v1, + opts + ) + when is_list(opts) do + where_fun = opts[:where] + + if Keyword.has_key?(opts, :by) do + partitions = partition_for_opts(opts[:by]) + edges_in_partitions(g, v1, partitions, where_fun) + else + g + |> edges(v1) + |> filter_edges(where_fun) + end + end + def edges(%__MODULE__{type: type, edges: meta, vertex_identifier: vertex_identifier}, v1, v2) do with v1_id <- vertex_identifier.(v1), v2_id <- vertex_identifier.(v2), @@ -530,19 +638,113 @@ defmodule Graph do end end + defp edges_in_partitions(g, partitions, where_fun) do + partitions + |> Enum.reduce(MapSet.new(), fn partition, acc -> + g.edge_index + |> Map.get(partition, %{}) + |> Map.values() + |> Enum.reduce(acc, fn partitioned_set, pacc -> + MapSet.union(partitioned_set, pacc) + end) + end) + |> Enum.flat_map(fn {v1_id, v2_id} = edge_key -> + v1 = Map.get(g.vertices, v1_id) + v2 = Map.get(g.vertices, v2_id) + + g.edges + |> Map.get(edge_key, []) + |> Enum.reduce([], fn {label, edge_meta}, acc -> + edge = + Edge.new(v1, v2, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) + + edge_partitions = g.partition_by.(edge) + + if include_edge_for_filtered_partitions?(edge, edge_partitions, partitions, where_fun) do + [edge | acc] + else + acc + end + end) + end) + end + + defp edges_in_partitions(g, v1, partitions, where_fun) do + v1_id = g.vertex_identifier.(v1) + + out_edges_set = + g.out_edges + |> Map.get(v1_id, MapSet.new()) + |> MapSet.new(fn v2_id -> + {v1_id, v2_id} + end) + + in_edges_set = + g.in_edges + |> Map.get(v1_id, MapSet.new()) + |> MapSet.new(fn v2_id -> + {v2_id, v1_id} + end) + + edges = MapSet.union(out_edges_set, in_edges_set) + + edge_adjacency_set = + partitions + |> Enum.reduce(MapSet.new(), fn partition, acc -> + g.edge_index + |> Map.get(partition, %{}) + |> Map.get(v1_id, MapSet.new()) + |> MapSet.union(acc) + end) + |> MapSet.intersection(edges) + + Enum.flat_map(edge_adjacency_set, fn {_v1_id, v2_id} = edge_key -> + v2 = Map.get(g.vertices, v2_id) + + g.edges + |> Map.get(edge_key, []) + |> Enum.reduce([], fn {label, edge_meta}, acc -> + edge = + Edge.new(v1, v2, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) + + edge_partitions = g.partition_by.(edge) + + if include_edge_for_filtered_partitions?(edge, edge_partitions, partitions, where_fun) do + [edge | acc] + else + acc + end + end) + end) + end + + defp filter_edges(edges, nil), do: edges + + defp filter_edges(edges, where_fun) do + Enum.filter(edges, where_fun) + end + defp edge_list(v1, v2, edge_meta, :undirected) do - for {label, weight} <- edge_meta do + for {label, %{weight: weight, properties: properties}} <- edge_meta do if v1 > v2 do - Edge.new(v2, v1, label: label, weight: weight) + Edge.new(v2, v1, label: label, weight: weight, properties: properties) else - Edge.new(v1, v2, label: label, weight: weight) + Edge.new(v1, v2, label: label, weight: weight, properties: properties) end end end defp edge_list(v1, v2, edge_meta, _) do - for {label, weight} <- edge_meta do - Edge.new(v1, v2, label: label, weight: weight) + for {label, %{weight: weight, properties: properties}} <- edge_meta do + Edge.new(v1, v2, label: label, weight: weight, properties: properties) end end @@ -590,8 +792,8 @@ defmodule Graph do v2_id <- vertex_identifier.(v2), edge_key <- {v1_id, v2_id}, {:ok, edge_meta} <- Map.fetch(meta, edge_key), - {:ok, weight} <- Map.fetch(edge_meta, label) do - Edge.new(v1, v2, label: label, weight: weight) + {:ok, %{weight: weight, properties: properties}} <- Map.fetch(edge_meta, label) do + Edge.new(v1, v2, label: label, weight: weight, properties: properties) else _ -> nil @@ -937,8 +1139,14 @@ defmodule Graph do [%Graph.Edge{v1: :a, v2: :b}] """ @spec add_edge(t, Edge.t()) :: t - def add_edge(%__MODULE__{} = g, %Edge{v1: v1, v2: v2, label: label, weight: weight}) do - add_edge(g, v1, v2, label: label, weight: weight) + def add_edge(%__MODULE__{} = g, %Edge{ + v1: v1, + v2: v2, + label: label, + weight: weight, + properties: properties + }) do + add_edge(g, v1, v2, label: label, weight: weight, properties: properties) end @doc """ @@ -997,8 +1205,17 @@ defmodule Graph do end edge_meta = Map.get(meta, {v1_id, v2_id}, %{}) - {label, weight} = Edge.options_to_meta(opts) - edge_meta = Map.put(edge_meta, label, weight) + {label, options_meta} = Edge.options_to_meta(opts) + edge_meta = Map.put(edge_meta, label, options_meta) + + g = + if g.multigraph do + edge = Edge.new(v1, v2, label: label, weight: options_meta.weight, properties: opts) + + index_multigraph_edge(g, {v1_id, v2_id}, edge) + else + g + end %__MODULE__{ g @@ -1008,12 +1225,45 @@ defmodule Graph do } end + defp index_multigraph_edge( + %__MODULE__{multigraph: true} = graph, + {v1_id, v2_id}, + %Edge{} = edge + ) do + partitions = graph.partition_by.(edge) + + Enum.reduce(partitions, graph, fn partition, g -> + edge_partition = Map.get(g.edge_index, partition, %{}) + + v1_set = Map.get(edge_partition, v1_id, MapSet.new()) + v2_set = Map.get(edge_partition, v2_id, MapSet.new()) + + new_edge_partition = + edge_partition + |> Map.put( + v1_id, + MapSet.put(v1_set, {v1_id, v2_id}) + ) + |> Map.put( + v2_id, + MapSet.put(v2_set, {v1_id, v2_id}) + ) + + %__MODULE__{ + g + | edge_index: + g.edge_index + |> Map.put(partition, new_edge_partition) + } + end) + end + @doc """ This function is like `add_edge/3`, but for multiple edges at once, it also accepts edge specifications in a few different ways to make it easy to generate graphs succinctly. Edges must be provided as a list of `Edge` structs, `{vertex, vertex}` pairs, or - `{vertex, vertex, edge_opts :: [label: term, weight: integer]}`. + `{vertex, vertex, edge_opts :: [label: term, weight: integer, properties: map]}`. See the docs for `Graph.Edge.new/2` or `Graph.Edge.new/3` for more info on creating Edge structs, and `add_edge/3` for information on edge options. @@ -1030,7 +1280,7 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edges([{:a, :b}, {:a, :b, label: :foo}, {:a, :b, label: :foo, weight: 2}]) ...> Graph.edges(g) - [%Graph.Edge{v1: :a, v2: :b, label: :foo, weight: 2}, %Graph.Edge{v1: :a, v2: :b}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: nil, properties: %{}}, %Graph.Edge{label: :foo, properties: %{}, v1: :a, v2: :b, weight: 2}] iex> Graph.new |> Graph.add_vertices([:a, :b, :c]) |> Graph.add_edges([:a, :b]) ** (Graph.EdgeSpecificationError) Expected a valid edge specification, but got: :a @@ -1107,10 +1357,10 @@ defmodule Graph do g = add_vertex(g, v3) - Enum.reduce(meta, g, fn {label, weight}, acc -> + Enum.reduce(meta, g, fn {label, %{weight: weight, properties: properties}}, acc -> acc - |> add_edge(v1, v3, label: label, weight: weight) - |> add_edge(v3, v2, label: label, weight: weight) + |> add_edge(v1, v3, label: label, weight: weight, properties: properties) + |> add_edge(v3, v2, label: label, weight: weight, properties: properties) end) else _ -> {:error, :no_such_edge} @@ -1128,7 +1378,7 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edge(:a, :b) |> Graph.add_edge(:a, :b, label: :bar) ...> %Graph{} = g = Graph.update_edge(g, :a, :b, weight: 2, label: :foo) ...> Graph.edges(g) - [%Graph.Edge{v1: :a, v2: :b, label: :bar}, %Graph.Edge{v1: :a, v2: :b, label: :foo, weight: 2}] + [%Graph.Edge{v1: :a, v2: :b, label: :foo, weight: 2}, %Graph.Edge{v1: :a, v2: :b, label: :bar}] """ @spec update_edge(t, vertex, vertex, Edge.edge_opts()) :: t | {:error, :no_such_edge} def update_edge(%__MODULE__{} = g, v1, v2, opts) when is_list(opts) do @@ -1145,12 +1395,12 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edge(:a, :b) |> Graph.add_edge(:a, :b, label: :bar) ...> %Graph{} = g = Graph.update_labelled_edge(g, :a, :b, :bar, weight: 2, label: :foo) ...> Graph.edges(g) - [%Graph.Edge{v1: :a, v2: :b, label: :foo, weight: 2}, %Graph.Edge{v1: :a, v2: :b}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: nil, properties: %{}}, %Graph.Edge{label: :foo, properties: %{}, v1: :a, v2: :b, weight: 2}] iex> g = Graph.new(type: :undirected) |> Graph.add_edge(:a, :b) |> Graph.add_edge(:a, :b, label: :bar) ...> %Graph{} = g = Graph.update_labelled_edge(g, :a, :b, :bar, weight: 2, label: :foo) ...> Graph.edges(g) - [%Graph.Edge{v1: :a, v2: :b, label: :foo, weight: 2}, %Graph.Edge{v1: :a, v2: :b}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: nil, properties: %{}}, %Graph.Edge{label: :foo, properties: %{}, v1: :a, v2: :b, weight: 2}] """ @spec update_labelled_edge(t, vertex, vertex, label, Edge.edge_opts()) :: t | {:error, :no_such_edge} @@ -1179,19 +1429,32 @@ defmodule Graph do edge_key <- {v1_id, v2_id}, {:ok, meta} <- Map.fetch(em, edge_key), {:ok, _} <- Map.fetch(meta, old_label), - {new_label, new_weight} <- Edge.options_to_meta(opts) do + {new_label, new_attrs} <- Edge.options_to_meta(opts) do case new_label do ^old_label -> - new_meta = Map.put(meta, old_label, new_weight) + new_meta = Map.put(meta, old_label, new_attrs) %__MODULE__{g | edges: Map.put(em, edge_key, new_meta)} nil -> - new_meta = Map.put(meta, old_label, new_weight) + new_meta = Map.put(meta, old_label, new_attrs) %__MODULE__{g | edges: Map.put(em, edge_key, new_meta)} _ -> - new_meta = Map.put(Map.delete(meta, old_label), new_label, new_weight) - %__MODULE__{g | edges: Map.put(em, edge_key, new_meta)} + new_meta = Map.put(Map.delete(meta, old_label), new_label, new_attrs) + + if g.multigraph do + g = + g + |> prune_edge_index({v1_id, v1}, {v2_id, v2}, old_label) + |> index_multigraph_edge( + {v1_id, v2_id}, + Edge.new(v1, v2, label: new_label, weight: new_attrs.weight, properties: opts) + ) + + %__MODULE__{g | edges: Map.put(em, edge_key, new_meta)} + else + %__MODULE__{g | edges: Map.put(em, edge_key, new_meta)} + end end else _ -> @@ -1246,6 +1509,7 @@ defmodule Graph do edge_key <- {v1_id, v2_id}, {:ok, v1_out} <- Map.fetch(oe, v1_id), {:ok, v2_in} <- Map.fetch(ie, v2_id) do + g = prune_edge_index(g, {v1_id, v1}, {v2_id, v2}, nil) v1_out = MapSet.delete(v1_out, v2_id) v2_in = MapSet.delete(v2_in, v1_id) meta = Map.delete(meta, edge_key) @@ -1261,6 +1525,92 @@ defmodule Graph do end end + defp prune_edge_index( + %__MODULE__{ + multigraph: true, + edge_index: edge_index, + edges: meta, + partition_by: partition_by + } = g, + {v1_id, v1}, + {v2_id, v2}, + nil + ) do + meta + |> Map.get({v1_id, v2_id}) + |> Enum.reduce(g, fn {label, edge_meta}, acc -> + edge = + Edge.new(v1, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) + + edge_partitions = partition_by.(edge) + + Enum.reduce(edge_partitions, acc, fn edge_p, acc -> + v1_key = {v1_id, edge_p} + v2_key = {v2_id, edge_p} + + edge_index = + edge_index + |> Map.delete(v1_key) + |> Map.delete(v2_key) + + %__MODULE__{ + acc + | edge_index: edge_index + } + end) + end) + end + + defp prune_edge_index( + %__MODULE__{ + multigraph: true, + edge_index: edge_index, + edges: meta, + partition_by: partition_by + } = g, + {v1_id, v1}, + {v2_id, v2}, + label + ) do + [{_label, edge_meta} | _] = + meta + |> Map.get({v1_id, v2_id}) + |> Enum.filter(fn {edge_label, _v} -> + edge_label == label + end) + + edge = + Edge.new(v1, v2, label: label, weight: edge_meta.weight, properties: edge_meta.properties) + + edge_partitions = partition_by.(edge) + + Enum.reduce(edge_partitions, g, fn edge_p, acc -> + partition = + edge_index + |> Map.get(edge_p, %{}) + |> Map.reject(fn {k, v} -> + (k == v1_id and MapSet.member?(v, {v1_id, v2_id})) or + (k == v2_id and MapSet.member?(v, {v1_id, v2_id})) + end) + + edge_index = + if not Enum.empty?(partition) do + Map.put(edge_index, edge_p, partition) + else + Map.delete(edge_index, edge_p) + end + + %__MODULE__{ + acc + | edge_index: edge_index + } + end) + end + + defp prune_edge_index(%__MODULE__{multigraph: false} = g, _v1, _v2, _label) do + g + end + @doc """ Removes an edge connecting `v1` to `v2`. A label can be specified to disambiguate the specific edge you wish to delete, if not provided, the unlabelled edge, if one exists, @@ -1319,6 +1669,7 @@ defmodule Graph do {:ok, v2_in} <- Map.fetch(ie, v2_id), {:ok, edge_meta} <- Map.fetch(meta, edge_key), {:ok, _} <- Map.fetch(edge_meta, label) do + g = prune_edge_index(g, {v1_id, v1}, {v2_id, v2}, label) edge_meta = Map.delete(edge_meta, label) case map_size(edge_meta) do @@ -2117,7 +2468,7 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edges([{:a, :b}, {:a, :b, label: :foo}, {:b, :c}]) ...> Graph.in_edges(g, :b) - [%Graph.Edge{v1: :a, v2: :b, label: :foo}, %Graph.Edge{v1: :a, v2: :b}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: nil, properties: %{}}, %Graph.Edge{label: :foo, properties: %{}, v1: :a, v2: :b, weight: 1}] """ @spec in_edges(t, vertex) :: Edge.t() def in_edges(%__MODULE__{type: :undirected} = g, v) do @@ -2138,8 +2489,12 @@ defmodule Graph do Enum.flat_map(v_in, fn v1_id -> v1 = Map.get(vs, v1_id) - Enum.map(Map.get(meta, {v1_id, v_id}), fn {label, weight} -> - Edge.new(v1, v, label: label, weight: weight) + Enum.map(Map.get(meta, {v1_id, v_id}), fn {label, edge_meta} -> + Edge.new(v1, v, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) end) end) else @@ -2147,6 +2502,59 @@ defmodule Graph do end end + def in_edges( + %__MODULE__{ + vertices: vs, + edges: edges, + in_edges: ie, + multigraph: true, + vertex_identifier: vertex_identifier, + edge_index: edge_index, + partition_by: partition_by + }, + v, + by: partition + ) do + v2_id = vertex_identifier.(v) + + in_edges_set = + ie + |> Map.get(v2_id, MapSet.new()) + |> MapSet.new(fn v1_id -> + {v1_id, v2_id} + end) + + in_edge_adjacency_set = + edge_index + |> Map.get(partition, %{}) + |> Map.get(v2_id, MapSet.new()) + |> MapSet.intersection(in_edges_set) + + Enum.flat_map(in_edge_adjacency_set, fn {v1_id, _v2_id} = edge_key -> + v1 = Map.get(vs, v1_id) + + edges + |> Map.get(edge_key, []) + |> Enum.map(fn {label, edge_meta} -> + edge = + Edge.new(v1, v, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) + + edge_partitions = partition_by.(edge) + + if Enum.any?(edge_partitions, fn edge_partition -> edge_partition == partition end) do + edge + else + nil + end + end) + |> Enum.reject(&is_nil/1) + end) + end + @doc """ Returns a list of vertices which the given vertex `v` has edges going to. @@ -2184,7 +2592,7 @@ defmodule Graph do iex> g = Graph.new |> Graph.add_edges([{:a, :b}, {:a, :b, label: :foo}, {:b, :c}]) ...> Graph.out_edges(g, :a) - [%Graph.Edge{v1: :a, v2: :b, label: :foo}, %Graph.Edge{v1: :a, v2: :b}] + [%Graph.Edge{v1: :a, v2: :b, weight: 1, label: nil, properties: %{}}, %Graph.Edge{label: :foo, properties: %{}, v1: :a, v2: :b, weight: 1}] """ @spec out_edges(t, vertex) :: Edge.t() def out_edges(%__MODULE__{type: :undirected} = g, v) do @@ -2205,8 +2613,12 @@ defmodule Graph do Enum.flat_map(v_out, fn v2_id -> v2 = Map.get(vs, v2_id) - Enum.map(Map.get(meta, {v_id, v2_id}), fn {label, weight} -> - Edge.new(v, v2, label: label, weight: weight) + Enum.map(Map.get(meta, {v_id, v2_id}), fn {label, edge_meta} -> + Edge.new(v, v2, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) end) end) else @@ -2215,6 +2627,101 @@ defmodule Graph do end end + @spec out_edges(Graph.t(), any(), [{:by, any()}, ...]) :: list() + def out_edges(%__MODULE__{multigraph: true} = g, v, opts) + when is_list(opts) do + where_fun = opts[:where] + + if Keyword.has_key?(opts, :by) do + partitions = partition_for_opts(opts[:by]) + + out_edges_in_partitions(g, v, partitions, where_fun) + else + g + |> out_edges(v) + |> filter_edges(where_fun) + end + end + + defp partition_for_opts(partition) when is_list(partition) do + partition + end + + defp partition_for_opts(partition) do + [partition] + end + + defp out_edges_in_partitions( + %__MODULE__{ + vertices: vs, + edges: edges, + out_edges: oe, + multigraph: true, + edge_index: edge_index, + vertex_identifier: vertex_identifier, + partition_by: partition_by + }, + v, + partitions, + where_fun + ) do + v1_id = vertex_identifier.(v) + + out_edges_set = + oe + |> Map.get(v1_id, MapSet.new()) + |> MapSet.new(fn v2_id -> + {v1_id, v2_id} + end) + + out_edge_adjacency_set = + partitions + |> Enum.reduce(MapSet.new(), fn partition, acc -> + edge_index + |> Map.get(partition, %{}) + |> Map.get(v1_id, MapSet.new()) + |> MapSet.union(acc) + end) + |> MapSet.intersection(out_edges_set) + + Enum.flat_map(out_edge_adjacency_set, fn {_v1_id, v2_id} = edge_key -> + v2 = Map.get(vs, v2_id) + + edges + |> Map.get(edge_key, []) + |> Enum.reduce([], fn {label, edge_meta}, acc -> + edge = + Edge.new(v, v2, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) + + edges_in_partitions = partition_by.(edge) + + if include_edge_for_filtered_partitions?(edge, edges_in_partitions, partitions, where_fun) do + [edge | acc] + else + acc + end + end) + end) + end + + defp include_edge_for_filtered_partitions?(_edge, edge_partitions, partitions, nil = _where_fun) do + Enum.any?(edge_partitions, fn ep -> ep in partitions end) + end + + defp include_edge_for_filtered_partitions?(edge, edge_partitions, partitions, where_fun) + when is_function(where_fun) do + Enum.any?(edge_partitions, fn ep -> ep in partitions and where_fun.(edge) end) + end + + defp include_edge_for_filtered_partitions?(edge, _edge_partition, _partitions, where_fun) + when is_function(where_fun) do + where_fun.(edge) + end + @doc """ Builds a maximal subgraph of `g` which includes all of the vertices in `vs` and the edges which connect them. @@ -2251,8 +2758,12 @@ defmodule Graph do |> Enum.reduce(sg, fn v2_id, sg -> v2 = Map.get(vertices, v2_id) - Enum.reduce(Map.get(meta, {v_id, v2_id}), sg, fn {label, weight}, sg -> - Graph.add_edge(sg, v, v2, label: label, weight: weight) + Enum.reduce(Map.get(meta, {v_id, v2_id}), sg, fn {label, edge_meta}, sg -> + Graph.add_edge(sg, v, v2, + label: label, + weight: edge_meta.weight, + properties: edge_meta.properties + ) end) end) end) diff --git a/lib/graph/pathfinding.ex b/lib/graph/pathfinding.ex index c7f7fa6..b21724a 100644 --- a/lib/graph/pathfinding.ex +++ b/lib/graph/pathfinding.ex @@ -6,7 +6,8 @@ defmodule Graph.Pathfinding do @type heuristic_fun :: (Graph.vertex() -> integer) - @spec bellman_ford(Graph.t, Graph.vertex) :: %{Graph.vertex() => integer() | :infinity} | nil + @spec bellman_ford(Graph.t(), Graph.vertex()) :: + %{Graph.vertex() => integer() | :infinity} | nil def bellman_ford(g, a), do: Graph.Pathfindings.BellmanFord.call(g, a) @doc """ diff --git a/lib/graph/pathfindings/bellman_ford.ex b/lib/graph/pathfindings/bellman_ford.ex index d5a421b..985018c 100644 --- a/lib/graph/pathfindings/bellman_ford.ex +++ b/lib/graph/pathfindings/bellman_ford.ex @@ -51,7 +51,8 @@ defmodule Graph.Pathfindings.BellmanFord do end @spec edge_weight(term) :: float - defp edge_weight({e, edge_value}), do: {e, edge_value |> Map.values() |> List.first()} + defp edge_weight({e, edge_value}), + do: {e, edge_value |> Map.values() |> List.first() |> Map.get(:weight)} defp has_negative_cycle?(distances, meta) do Enum.any?(meta, fn {{u, v}, weight} -> diff --git a/lib/graph/serializers/dot.ex b/lib/graph/serializers/dot.ex index 0df88c7..baf5dd4 100644 --- a/lib/graph/serializers/dot.ex +++ b/lib/graph/serializers/dot.ex @@ -14,7 +14,9 @@ defmodule Graph.Serializers.DOT do defp serialize_nodes(%Graph{vertices: vertices} = g) do Enum.reduce(vertices, "", fn {id, v}, acc -> - acc <> Serializer.indent(1) <> "#{id}" <> "[label=" <> Serializer.get_vertex_label(g, id, v) <> "]\n" + acc <> + Serializer.indent(1) <> + "#{id}" <> "[label=" <> Serializer.get_vertex_label(g, id, v) <> "]\n" end) end @@ -26,10 +28,10 @@ defmodule Graph.Serializers.DOT do |> Map.get(id, MapSet.new()) |> Enum.flat_map(fn id2 -> Enum.map(Map.fetch!(em, {id, id2}), fn - {nil, weight} -> + {nil, %{weight: weight}} -> {id, id2, weight} - {label, weight} -> + {label, %{weight: weight}} -> {id, id2, weight, Serializer.encode_label(label)} end) end) diff --git a/lib/graph/serializers/flowchart.ex b/lib/graph/serializers/flowchart.ex index 392a940..7a65cb8 100644 --- a/lib/graph/serializers/flowchart.ex +++ b/lib/graph/serializers/flowchart.ex @@ -38,8 +38,8 @@ defmodule Graph.Serializers.Flowchart do g.edges |> Map.fetch!({id, out_edge_id}) |> Enum.map(fn - {nil, weight} -> {id, out_edge_id, weight} - {label, weight} -> {id, out_edge_id, weight, encode_label(label)} + {nil, %{weight: weight}} -> {id, out_edge_id, weight} + {label, %{weight: weight}} -> {id, out_edge_id, weight, encode_label(label)} end) end) |> case do diff --git a/lib/graph/utils.ex b/lib/graph/utils.ex index f53b987..2260fc2 100644 --- a/lib/graph/utils.ex +++ b/lib/graph/utils.ex @@ -83,7 +83,7 @@ defmodule Graph.Utils do def edge_weight(%Graph{type: :directed, edges: meta}, a, b) do Map.fetch!(meta, {a, b}) - |> Enum.map(fn {_label, weight} -> weight end) + |> Enum.map(fn {_label, %{weight: weight}} -> weight end) |> Enum.min() end @@ -96,13 +96,13 @@ defmodule Graph.Utils do edge_meta when is_map(edge_meta) -> edge_meta - |> Enum.map(fn {_label, weight} -> weight end) + |> Enum.map(fn {_label, %{weight: weight}} -> weight end) |> Enum.min() end edge_meta when is_map(edge_meta) -> edge_meta - |> Enum.map(fn {_label, weight} -> weight end) + |> Enum.map(fn {_label, %{weight: weight}} -> weight end) |> Enum.min() end end @@ -110,4 +110,6 @@ defmodule Graph.Utils do # 2^32 @max_phash 4_294_967_296 def vertex_id(v), do: :erlang.phash2(v, @max_phash) + + def by_edge_label(%{label: label}), do: [label] end diff --git a/test/graph_test.exs b/test/graph_test.exs index 2b73f02..679eb17 100644 --- a/test/graph_test.exs +++ b/test/graph_test.exs @@ -20,6 +20,213 @@ defmodule GraphTest do assert Graph.has_vertex?(g_with_custom_vertex_identifier, :v1) end + describe "multigraphs" do + test "`multigraph: true` option enables edge indexing on edge labels" do + graph = + Graph.new(multigraph: true) + |> Graph.add_edges([ + {:a, :b}, + {:a, :b, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, label: {:complex, :label}} + ]) + + assert Enum.count(Graph.out_edges(graph, :a)) == 3 + assert [%Edge{label: :foo}] = Graph.out_edges(graph, :a, by: [:foo]) + assert [%Edge{label: :foo}] = Graph.out_edges(graph, :a, by: :foo) + assert [%Edge{label: :foo}] = Graph.in_edges(graph, :b, by: :foo) + assert [%Edge{label: :bar}] = Graph.out_edges(graph, :a, by: :bar) + assert [%Edge{label: nil}] = Graph.out_edges(graph, :a, by: nil) + + assert [%Edge{label: nil}] = Graph.out_edges(graph, :a, by: nil) + + assert [%Edge{label: {:complex, :label}}] = + Graph.out_edges(graph, :b, + where: fn edge -> edge.label == {:complex, :label} or edge.label == :bar end + ) + + assert 1 == graph |> Graph.edges(by: :foo) |> Enum.count() + assert 1 == graph |> Graph.edges(where: fn edge -> edge.weight > 2 end) |> Enum.count() + + assert 1 == + graph + |> Graph.edges(:a, by: :foo) + |> Enum.count() + + assert 2 == + graph + |> Graph.edges(by: [:foo, :bar]) + |> Enum.count() + + assert 1 == + graph + |> Graph.edges(by: [:foo, :bar], where: fn edge -> edge.label == :bar end) + |> Enum.count() + + assert [] == Graph.out_edges(graph, :a, by: :foobar) + end + + test "custom edge partition_by function" do + graph = + Graph.new(multigraph: true, partition_by: fn edge -> [edge.weight] end) + |> Graph.add_edges([ + {:a, :b}, + {:a, :b, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, weight: 6} + ]) + + assert Enum.count(Graph.out_edges(graph, :b)) == 2 + + assert [%Edge{weight: 6}] = + Graph.out_edges(graph, :b, where: fn edge -> edge.weight == 6 end) + + assert [%Edge{weight: 3}] = + Graph.out_edges(graph, :b, where: fn edge -> edge.weight == 3 end) + end + + test "custom partition_by supports indexing to more than one partition" do + graph = + Graph.new(multigraph: true, partition_by: fn edge -> [edge.weight, edge.label] end) + |> Graph.add_edges([ + {:a, :b}, + {:a, :d, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, weight: 6, label: :foo} + ]) + + assert Enum.count(Graph.out_edges(graph, :b)) == 2 + + assert [%Edge{weight: 6, label: :foo}] = + Graph.out_edges(graph, :b, by: 6) + + assert Enum.count(Graph.edges(graph, by: [:foo])) == 2 + end + + test "removing edges prunes index" do + g = + Graph.new(multigraph: true) + |> Graph.add_edges([ + {:a, :b}, + {:a, :b, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, label: {:complex, :label}} + ]) + + g = Graph.delete_edges(g, [{:b, :c}, {:b, :a}]) + refute Map.has_key?(g.edge_index, {g.vertex_identifier.(:b), {:complex, :label}}) + end + + test "delete_edge/3 removes only a multigraph's properties and index for the given partition key/label" do + g = + Graph.new(multigraph: true) + |> Graph.add_edges([ + {:a, :b}, + {:a, :b, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, label: {:complex, :label}} + ]) + + g = Graph.delete_edge(g, :a, :b, :foo) + + refute Map.has_key?(g.edge_index, :foo) + assert Enum.empty?(Graph.out_edges(g, :a, by: :foo)) + assert Enum.empty?(Graph.edges(g, by: :foo)) + end + + test "update_labelled_edge/3 updates an indexed adge with new label" do + g = + Graph.new(multigraph: true) + |> Graph.add_edges([ + {:a, :b}, + {:a, :b, label: :foo}, + {:a, :b, label: :bar}, + {:b, :c, weight: 3}, + {:b, :a, label: {:complex, :label}} + ]) + + g = Graph.update_labelled_edge(g, :a, :b, :foo, label: :baz) + + refute Map.has_key?(g.edge_index, :foo) + assert Map.has_key?(g.edge_index[:baz], g.vertex_identifier.(:a)) + end + + # test "BFS traversal using multigraph partitions" do + # graph = + # Graph.new(multigraph: true) + # |> Graph.add_edges([ + # {:a, :b, label: :foo}, + # {:a, :b, label: :bar}, + # {:b, :c, weight: 3}, + # {:b, :a, label: {:complex, :label}} + # ]) + + # assert Graph.bfs(graph, :a) == [:a, :b, :c] + # end + + # test "DFS traversal using multigraph partitions" do + # end + + # test "Dijkstra traversal using multigraph partitions" do + # end + + # test "A* traversal using multigraph partitions" do + # end + + # test "Bellman-Ford traversal using multigraph partitions" do + # end + end + + describe "edge properties" do + test "setting edge properties" do + g = + Graph.new() + |> Graph.add_edges([ + {:a, :b, properties: %{foo: :bar}}, + {:a, :b, label: :foo, properties: %{bar: :foo}} + ]) + + assert [ + %Edge{v1: :a, v2: :b, properties: %{foo: :bar}}, + %Edge{v1: :a, v2: :b, label: :foo, properties: %{bar: :foo}} + ] = Graph.out_edges(g, :a) + end + + test "updating edge properties" do + g = + Graph.new() + |> Graph.add_edges([ + {:a, :b, properties: %{foo: :bar}}, + {:a, :b, label: :foo, properties: %{bar: :foo}} + ]) + |> Graph.update_edge(:a, :b, properties: %{ham: :potato}) + |> Graph.update_labelled_edge(:a, :b, :foo, properties: %{potato: :ham}) + + assert [ + %Edge{v1: :a, v2: :b, properties: %{ham: :potato}}, + %Edge{v1: :a, v2: :b, label: :foo, properties: %{potato: :ham}} + ] = Graph.out_edges(g, :a) + end + + test "adding edge struct with properties" do + g = + Graph.new() + + edge = Edge.new(:a, :b, properties: %{foo: :bar}) + + g = Graph.add_edge(g, edge) + + assert [ + %Edge{v1: :a, v2: :b, properties: %{foo: :bar}} + ] = Graph.out_edges(g, :a) + end + end + test "delete vertex" do g = Graph.new() g = Graph.add_vertex(g, :v1, :labelA) @@ -76,12 +283,12 @@ defmodule GraphTest do # pretty printed str = "#{inspect(g)}" - assert "#Graph :b, :a -> :b, :b -[{:complex, :label}]-> :a, :b -> :c]>" = + assert "#Graph :b, :a -[foo]-> :b, :b -[{:complex, :label}]-> :a, :b -> :c]>" = str ustr = "#{inspect(ug)}" - assert "#Graph :b, :a <-> :b, :a <-[{:complex, :label}]-> :b, :b <-> :c]>" = + assert "#Graph :b, :a <-[foo]-> :b, :a <-[{:complex, :label}]-> :b, :b <-> :c]>" = ustr # large graph @@ -147,13 +354,11 @@ defmodule GraphTest do ]) |> Graph.edges(:a) - expected_result = [ - %Graph.Edge{label: "label3", v1: :b, v2: :a, weight: 1}, - %Graph.Edge{label: "label1", v1: :a, v2: :b, weight: 1}, - %Graph.Edge{label: "label2", v1: :a, v2: :b, weight: 1} - ] - - assert generated_result == expected_result + for edge <- generated_result do + assert edge.label in ["label1", "label2", "label3"] and + ((edge.v1 == :a and edge.v2 == :b) or + (edge.v1 == :b and edge.v2 == :a)) + end end test "is_subgraph?" do @@ -636,7 +841,7 @@ defmodule GraphTest do end defp build_complex_signed_graph do - Graph.new + Graph.new() |> Graph.add_edge(:a, :b, weight: -1) |> Graph.add_edge(:b, :e, weight: 2) |> Graph.add_edge(:e, :d, weight: -3) diff --git a/test/priority_queue_test.exs b/test/priority_queue_test.exs index ab6b632..20d5d40 100644 --- a/test/priority_queue_test.exs +++ b/test/priority_queue_test.exs @@ -10,7 +10,7 @@ defmodule PriorityQueue.Test do end) str = "#{inspect(pq)}" - assert "#PriorityQueue" = str + assert "#PriorityQueue" = str end test "can enqueue random elements and pull them out in priority order" do diff --git a/test/reducer_test.exs b/test/reducer_test.exs index 713e32f..428024f 100644 --- a/test/reducer_test.exs +++ b/test/reducer_test.exs @@ -35,17 +35,19 @@ defmodule Graph.Reducer.Test do end test "can walk a graph breadth-first, when the starting points had their in-edges deleted" do - g = Graph.new - |> Graph.add_vertices([:a, :b, :c, :d, :e, :f, :g]) - |> Graph.add_edge(:a, :b) - |> Graph.add_edge(:a, :d) - |> Graph.add_edge(:b, :c) - |> Graph.add_edge(:b, :d) - |> Graph.add_edge(:c, :e) - |> Graph.add_edge(:d, :f) - |> Graph.add_edge(:f, :g) - |> Graph.add_edge(:b, :a) # Add this edge and then remove it - |> Graph.delete_edge(:b, :a) + g = + Graph.new() + |> Graph.add_vertices([:a, :b, :c, :d, :e, :f, :g]) + |> Graph.add_edge(:a, :b) + |> Graph.add_edge(:a, :d) + |> Graph.add_edge(:b, :c) + |> Graph.add_edge(:b, :d) + |> Graph.add_edge(:c, :e) + |> Graph.add_edge(:d, :f) + |> Graph.add_edge(:f, :g) + # Add this edge and then remove it + |> Graph.add_edge(:b, :a) + |> Graph.delete_edge(:b, :a) expected = [:a, :b, :d, :c, :f, :e, :g] assert ^expected = Graph.Reducers.Bfs.map(g, fn v -> v end) diff --git a/test/utils_test.exs b/test/utils_test.exs index 7a9a3f3..f72724c 100644 --- a/test/utils_test.exs +++ b/test/utils_test.exs @@ -7,7 +7,7 @@ defmodule Graph.UtilsTest do test "sizeof/1" do assert 64 = sizeof({1, :foo, "bar"}) - assert 440 = sizeof(String.duplicate("bar", 128)) + assert 456 = sizeof(String.duplicate("bar", 128)) assert 8 = sizeof([]) assert 24 = sizeof([1 | 2]) assert 56 = sizeof([1, 2, 3])