Current section
Files
Jump to
Current section
Files
src/yog@operation.erl
-module(yog@operation).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yog/operation.gleam").
-export([union/2, cartesian_product/4, compose/2, is_isomorphic/2, disjoint_union/2, power/3, is_subgraph/2, intersection/2, difference/2, symmetric_difference/2]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Graph operations - Set-theoretic operations, composition, and structural comparison.\n"
"\n"
" This module implements binary operations that treat graphs as sets of nodes and edges,\n"
" following NetworkX's \"Graph as a Set\" philosophy. These operations allow you to combine,\n"
" compare, and analyze structural differences between graphs.\n"
"\n"
" ## Set-Theoretic Operations\n"
"\n"
" | Function | Description | Use Case |\n"
" |----------|-------------|----------|\n"
" | `union/2` | All nodes and edges from both graphs | Combine graph data |\n"
" | `intersection/2` | Only nodes and edges common to both | Find common structure |\n"
" | `difference/2` | Nodes/edges in first but not second | Find unique structure |\n"
" | `symmetric_difference/2` | Edges in exactly one graph | Find differing structure |\n"
"\n"
" ## Composition & Joins\n"
"\n"
" | Function | Description | Use Case |\n"
" |----------|-------------|----------|\n"
" | `disjoint_union/2` | Combine with automatic ID re-indexing | Safe graph combination |\n"
" | `cartesian_product/2` | Multiply graphs (grids, hypercubes) | Generate complex structures |\n"
" | `compose/2` | Merge overlapping graphs with combined edges | Layered systems |\n"
" | `power/2` | k-th power (connect nodes within distance k) | Reachability analysis |\n"
"\n"
" ## Structural Comparison\n"
"\n"
" | Function | Description | Use Case |\n"
" |----------|-------------|----------|\n"
" | `is_subgraph/2` | Check if first is subset of second | Validation, pattern matching |\n"
" | `is_isomorphic/2` | Check if graphs are structurally identical | Graph comparison |\n"
"\n"
" ## Example: Combining Graphs Safely\n"
"\n"
" ```gleam\n"
" import yog\n"
" import yog/operation\n"
"\n"
" // Two triangle graphs with overlapping IDs\n"
" let triangle1 = yog.from_simple_edges(Directed, [#(0, 1), #(1, 2), #(2, 0)])\n"
" let triangle2 = yog.from_simple_edges(Directed, [#(0, 1), #(1, 2), #(2, 0)])\n"
"\n"
" // disjoint_union re-indexes the second graph automatically\n"
" let combined = operations.disjoint_union(triangle1, triangle2)\n"
" // Result: 6 nodes (0-5), two separate triangles\n"
" ```\n"
"\n"
" ## Example: Finding Common Structure\n"
"\n"
" ```gleam\n"
" let common = operations.intersection(graph_a, graph_b)\n"
" // Returns only nodes and edges present in both graphs\n"
" ```\n"
).
-file("src/yog/operation.gleam", 66).
?DOC(" Returns a graph containing all nodes and edges from both input graphs.\n").
-spec union(yog@model:graph(UFE, UFF), yog@model:graph(UFE, UFF)) -> yog@model:graph(UFE, UFF).
union(Base, Other) ->
yog@transform:merge(Base, Other).
-file("src/yog/operation.gleam", 131).
?DOC(" Returns the Cartesian product of two graphs.\n").
-spec cartesian_product(
yog@model:graph(UGS, UGT),
yog@model:graph(UGW, UGX),
UGX,
UGT
) -> yog@model:graph({UGS, UGW}, {UGT, UGX}).
cartesian_product(First, Second, Default_first, Default_second) ->
First_nodes = yog@model:all_nodes(First),
Second_nodes = yog@model:all_nodes(Second),
Second_order = yog@model:order(Second),
Init_graph = yog@model:new(erlang:element(2, First)),
Graph_with_nodes = gleam@list:fold(
First_nodes,
Init_graph,
fun(G_acc, U) ->
gleam@list:fold(
Second_nodes,
G_acc,
fun(G, V) ->
New_id = (U * Second_order) + V,
U_data@1 = case gleam_stdlib:map_get(
erlang:element(3, First),
U
) of
{ok, U_data} -> U_data;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"yog/operation"/utf8>>,
function => <<"cartesian_product"/utf8>>,
line => 147,
value => _assert_fail,
start => 5450,
'end' => 5498,
pattern_start => 5461,
pattern_end => 5471})
end,
V_data@1 = case gleam_stdlib:map_get(
erlang:element(3, Second),
V
) of
{ok, V_data} -> V_data;
_assert_fail@1 ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"yog/operation"/utf8>>,
function => <<"cartesian_product"/utf8>>,
line => 148,
value => _assert_fail@1,
start => 5507,
'end' => 5556,
pattern_start => 5518,
pattern_end => 5528})
end,
yog@model:add_node(G, New_id, {U_data@1, V_data@1})
end
)
end
),
Graph_with_second_edges = gleam@list:fold(
First_nodes,
Graph_with_nodes,
fun(G_acc@1, U@1) ->
gleam@list:fold(
Second_nodes,
G_acc@1,
fun(G@1, V@1) ->
V_successors = yog@model:successors(Second, V@1),
gleam@list:fold(
V_successors,
G@1,
fun(G_inner, Edge) ->
{V_succ, Edge_weight} = Edge,
Src_id = (U@1 * Second_order) + V@1,
Dst_id = (U@1 * Second_order) + V_succ,
Edge_data = {Default_second, Edge_weight},
case yog@model:add_edge(
G_inner,
Src_id,
Dst_id,
Edge_data
) of
{ok, New_g} ->
New_g;
{error, _} ->
G_inner
end
end
)
end
)
end
),
gleam@list:fold(
Second_nodes,
Graph_with_second_edges,
fun(G_acc@2, V@2) ->
gleam@list:fold(
First_nodes,
G_acc@2,
fun(G@2, U@2) ->
U_successors = yog@model:successors(First, U@2),
gleam@list:fold(
U_successors,
G@2,
fun(G_inner@1, Edge@1) ->
{U_succ, Edge_weight@1} = Edge@1,
Src_id@1 = (U@2 * Second_order) + V@2,
Dst_id@1 = (U_succ * Second_order) + V@2,
Edge_data@1 = {Edge_weight@1, Default_first},
case yog@model:add_edge(
G_inner@1,
Src_id@1,
Dst_id@1,
Edge_data@1
) of
{ok, New_g@1} ->
New_g@1;
{error, _} ->
G_inner@1
end
end
)
end
)
end
).
-file("src/yog/operation.gleam", 192).
?DOC(" Composes two graphs by merging overlapping nodes and combining their edges.\n").
-spec compose(yog@model:graph(UHC, UHD), yog@model:graph(UHC, UHD)) -> yog@model:graph(UHC, UHD).
compose(First, Second) ->
union(First, Second).
-file("src/yog/operation.gleam", 250).
-spec bfs_distances(
yog@model:graph(any(), any()),
list(integer()),
gleam@dict:dict(integer(), integer()),
integer()
) -> list(integer()).
bfs_distances(Graph, Queue, Distances, Max_dist) ->
case Queue of
[] ->
maps:keys(Distances);
[Current | Rest] ->
Current_dist = begin
_pipe = gleam_stdlib:map_get(Distances, Current),
gleam@result:unwrap(_pipe, Max_dist + 1)
end,
case Current_dist >= Max_dist of
true ->
bfs_distances(Graph, Rest, Distances, Max_dist);
false ->
Neighbors = yog@model:successor_ids(Graph, Current),
{New_queue, New_distances} = gleam@list:fold(
Neighbors,
{Rest, Distances},
fun(Acc, Neighbor) ->
{Q, Dists} = Acc,
case gleam@dict:has_key(Dists, Neighbor) of
true ->
Acc;
false ->
New_dist = Current_dist + 1,
case New_dist =< Max_dist of
true ->
{[Neighbor | Q],
gleam@dict:insert(
Dists,
Neighbor,
New_dist
)};
false ->
Acc
end
end
end
),
bfs_distances(Graph, New_queue, New_distances, Max_dist)
end
end.
-file("src/yog/operation.gleam", 242).
?DOC(" Finds all nodes within distance k from a source node using BFS.\n").
-spec nodes_within_distance(yog@model:graph(any(), any()), integer(), integer()) -> list(integer()).
nodes_within_distance(Graph, Src, Max_dist) ->
bfs_distances(Graph, [Src], maps:from_list([{Src, 0}]), Max_dist).
-file("src/yog/operation.gleam", 353).
?DOC(" Computes the degree sequence of a graph, dropping node uniqueness but preserving in/out combinations.\n").
-spec degree_sequence(yog@model:graph(any(), any())) -> list({integer(),
integer()}).
degree_sequence(Graph) ->
_pipe = yog@model:all_nodes(Graph),
gleam@list:map(
_pipe,
fun(Node) ->
Out_deg = erlang:length(yog@model:successor_ids(Graph, Node)),
In_deg = erlang:length(yog@model:predecessors(Graph, Node)),
{In_deg, Out_deg}
end
).
-file("src/yog/operation.gleam", 409).
?DOC(" Checks if mapping src -> candidate is consistent with current mapping.\n").
-spec is_mapping_valid(
yog@model:graph(UJK, UJL),
yog@model:graph(UJK, UJL),
integer(),
integer(),
gleam@dict:dict(integer(), integer())
) -> boolean().
is_mapping_valid(First, Second, Src, Candidate, Mapping) ->
Src_successors = yog@model:successor_ids(First, Src),
Candidate_successors = yog@model:successor_ids(Second, Candidate),
Inconsistent_edges = gleam@dict:fold(
Mapping,
0,
fun(Count, Src_neighbor, Candidate_neighbor) ->
case gleam@list:contains(Src_successors, Src_neighbor) of
false ->
Count;
true ->
case gleam@list:contains(
Candidate_successors,
Candidate_neighbor
) of
true ->
Count;
false ->
Count + 1
end
end
end
),
Src_predecessors = begin
_pipe = yog@model:predecessors(First, Src),
gleam@list:map(_pipe, fun(T) -> erlang:element(1, T) end)
end,
Candidate_predecessors = begin
_pipe@1 = yog@model:predecessors(Second, Candidate),
gleam@list:map(_pipe@1, fun(T@1) -> erlang:element(1, T@1) end)
end,
Inconsistent_incoming = gleam@dict:fold(
Mapping,
0,
fun(Count@1, Src_neighbor@1, Candidate_neighbor@1) ->
case gleam@list:contains(Src_predecessors, Src_neighbor@1) of
false ->
Count@1;
true ->
case gleam@list:contains(
Candidate_predecessors,
Candidate_neighbor@1
) of
true ->
Count@1;
false ->
Count@1 + 1
end
end
end
),
(Inconsistent_edges =:= 0) andalso (Inconsistent_incoming =:= 0).
-file("src/yog/operation.gleam", 375).
-spec try_mapping(
yog@model:graph(UJA, UJB),
yog@model:graph(UJA, UJB),
list(integer()),
list(integer()),
gleam@dict:dict(integer(), integer())
) -> boolean().
try_mapping(First, Second, Remaining_first, Available_second, Current_mapping) ->
case Remaining_first of
[] ->
true;
[Src | Rest] ->
Src_in = erlang:length(yog@model:predecessors(First, Src)),
Src_out = erlang:length(yog@model:successor_ids(First, Src)),
Valid_candidates = gleam@list:filter(
Available_second,
fun(Candidate) ->
Cand_in = erlang:length(
yog@model:predecessors(Second, Candidate)
),
Cand_out = erlang:length(
yog@model:successor_ids(Second, Candidate)
),
(Src_in =:= Cand_in) andalso (Src_out =:= Cand_out)
end
),
gleam@list:any(
Valid_candidates,
fun(Candidate@1) ->
case is_mapping_valid(
First,
Second,
Src,
Candidate@1,
Current_mapping
) of
false ->
false;
true ->
New_mapping = gleam@dict:insert(
Current_mapping,
Src,
Candidate@1
),
New_available = gleam@list:filter(
Available_second,
fun(N) -> N /= Candidate@1 end
),
try_mapping(
First,
Second,
Rest,
New_available,
New_mapping
)
end
end
)
end.
-file("src/yog/operation.gleam", 363).
?DOC(" Attempts to find an isomorphism between two graphs using backtracking.\n").
-spec attempt_isomorphism(yog@model:graph(UIU, UIV), yog@model:graph(UIU, UIV)) -> boolean().
attempt_isomorphism(First, Second) ->
Degree = fun(Node) ->
erlang:length(yog@model:predecessors(First, Node)) + erlang:length(
yog@model:successor_ids(First, Node)
)
end,
First_nodes = begin
_pipe = yog@model:all_nodes(First),
gleam@list:sort(
_pipe,
fun(A, B) -> gleam@int:compare(Degree(B), Degree(A)) end
)
end,
Second_nodes = yog@model:all_nodes(Second),
try_mapping(First, Second, First_nodes, Second_nodes, maps:new()).
-file("src/yog/operation.gleam", 316).
?DOC(" Checks if two graphs are isomorphic (structurally identical).\n").
-spec is_isomorphic(yog@model:graph(UIJ, UIK), yog@model:graph(UIJ, UIK)) -> boolean().
is_isomorphic(First, Second) ->
First_order = yog@model:order(First),
Second_order = yog@model:order(Second),
case First_order =:= Second_order of
false ->
false;
true ->
First_edges = yog@model:edge_count(First),
Second_edges = yog@model:edge_count(Second),
case First_edges =:= Second_edges of
false ->
false;
true ->
Degree_compare = fun(A, B) ->
case gleam@int:compare(
erlang:element(1, A),
erlang:element(1, B)
) of
eq ->
gleam@int:compare(
erlang:element(2, A),
erlang:element(2, B)
);
Other ->
Other
end
end,
First_degrees = begin
_pipe = degree_sequence(First),
gleam@list:sort(_pipe, Degree_compare)
end,
Second_degrees = begin
_pipe@1 = degree_sequence(Second),
gleam@list:sort(_pipe@1, Degree_compare)
end,
case First_degrees =:= Second_degrees of
false ->
false;
true ->
attempt_isomorphism(First, Second)
end
end
end.
-file("src/yog/operation.gleam", 456).
?DOC(" Shifts all node IDs in a graph by a given offset.\n").
-spec shift_node_ids(yog@model:graph(UJS, UJT), integer()) -> yog@model:graph(UJS, UJT).
shift_node_ids(Graph, Offset) ->
Nodes = yog@model:all_nodes(Graph),
Id_mapping = gleam@list:fold(
Nodes,
maps:new(),
fun(Acc, Node) -> gleam@dict:insert(Acc, Node, Node + Offset) end
),
New_nodes = gleam@dict:fold(
erlang:element(3, Graph),
maps:new(),
fun(Acc@1, Id, Data) ->
New_id = begin
_pipe = gleam_stdlib:map_get(Id_mapping, Id),
gleam@result:unwrap(_pipe, Id)
end,
gleam@dict:insert(Acc@1, New_id, Data)
end
),
New_out_edges = gleam@dict:fold(
erlang:element(4, Graph),
maps:new(),
fun(Acc@2, Src, Targets) ->
New_src = begin
_pipe@1 = gleam_stdlib:map_get(Id_mapping, Src),
gleam@result:unwrap(_pipe@1, Src)
end,
New_targets = gleam@dict:fold(
Targets,
maps:new(),
fun(Inner_acc, Dst, Weight) ->
New_dst = begin
_pipe@2 = gleam_stdlib:map_get(Id_mapping, Dst),
gleam@result:unwrap(_pipe@2, Dst)
end,
gleam@dict:insert(Inner_acc, New_dst, Weight)
end
),
gleam@dict:insert(Acc@2, New_src, New_targets)
end
),
New_in_edges = gleam@dict:fold(
erlang:element(5, Graph),
maps:new(),
fun(Acc@3, Dst@1, Sources) ->
New_dst@1 = begin
_pipe@3 = gleam_stdlib:map_get(Id_mapping, Dst@1),
gleam@result:unwrap(_pipe@3, Dst@1)
end,
New_sources = gleam@dict:fold(
Sources,
maps:new(),
fun(Inner_acc@1, Src@1, Weight@1) ->
New_src@1 = begin
_pipe@4 = gleam_stdlib:map_get(Id_mapping, Src@1),
gleam@result:unwrap(_pipe@4, Src@1)
end,
gleam@dict:insert(Inner_acc@1, New_src@1, Weight@1)
end
),
gleam@dict:insert(Acc@3, New_dst@1, New_sources)
end
),
{graph, erlang:element(2, Graph), New_nodes, New_out_edges, New_in_edges}.
-file("src/yog/operation.gleam", 124).
?DOC(" Combines two graphs assuming they are separate entities with automatic re-indexing.\n").
-spec disjoint_union(yog@model:graph(UGK, UGL), yog@model:graph(UGK, UGL)) -> yog@model:graph(UGK, UGL).
disjoint_union(Base, Other) ->
Offset = yog@model:order(Base),
Reindexed_other = shift_node_ids(Other, Offset),
union(Base, Reindexed_other).
-file("src/yog/operation.gleam", 507).
?DOC(" Removes nodes not in the given set.\n").
-spec remove_nodes_not_in_set(
yog@model:graph(UKF, UKG),
gleam@set:set(integer())
) -> yog@model:graph(UKF, UKG).
remove_nodes_not_in_set(Graph, Keep) ->
Nodes_to_remove = begin
_pipe = yog@model:all_nodes(Graph),
gleam@list:filter(
_pipe,
fun(Node) -> not gleam@set:contains(Keep, Node) end
)
end,
gleam@list:fold(
Nodes_to_remove,
Graph,
fun(G, Node@1) -> yog@model:remove_node(G, Node@1) end
).
-file("src/yog/operation.gleam", 501).
?DOC(" Filters nodes to only those in the given set.\n").
-spec filter_nodes_by_set(yog@model:graph(UJY, UJZ), gleam@set:set(integer())) -> yog@model:graph(UJY, UJZ).
filter_nodes_by_set(Graph, Keep) ->
_pipe = yog@transform:filter_nodes(Graph, fun(_) -> true end),
remove_nodes_not_in_set(_pipe, Keep).
-file("src/yog/operation.gleam", 554).
?DOC(" Checks if an edge exists in the graph.\n").
-spec edge_exists_in(yog@model:graph(any(), any()), integer(), integer()) -> boolean().
edge_exists_in(Graph, Src, Dst) ->
_pipe = yog@model:successors(Graph, Src),
gleam@list:any(_pipe, fun(Edge) -> erlang:element(1, Edge) =:= Dst end).
-file("src/yog/operation.gleam", 202).
?DOC(
" Returns the k-th power of a graph.\n"
"\n"
" The k-th power of a graph G, denoted G^k, is a graph where two nodes are\n"
" adjacent if and only if their distance in G is at most k.\n"
"\n"
" New edges are created with the provided `default_weight`.\n"
).
-spec power(yog@model:graph(UHK, UHL), integer(), UHL) -> yog@model:graph(UHK, UHL).
power(Graph, K, Default_weight) ->
case K =< 1 of
true ->
Graph;
false ->
Nodes = yog@model:all_nodes(Graph),
Result_with_edges = gleam@list:fold(
Nodes,
Graph,
fun(Acc_graph, Src) ->
Reachable = nodes_within_distance(Graph, Src, K),
gleam@list:fold(
Reachable,
Acc_graph,
fun(G, Dst) -> case Src =:= Dst of
true ->
G;
false ->
case edge_exists_in(G, Src, Dst) of
true ->
G;
false ->
case yog@model:add_edge(
G,
Src,
Dst,
Default_weight
) of
{ok, New_g} ->
New_g;
{error, _} ->
G
end
end
end end
)
end
),
Result_with_edges
end.
-file("src/yog/operation.gleam", 294).
?DOC(" Checks if the first graph is a subgraph of the second graph.\n").
-spec is_subgraph(yog@model:graph(UID, UIE), yog@model:graph(UID, UIE)) -> boolean().
is_subgraph(Potential, Container) ->
Potential_nodes = yog@model:all_nodes(Potential),
Container_nodes = gleam@set:from_list(yog@model:all_nodes(Container)),
All_nodes_exist = gleam@list:all(
Potential_nodes,
fun(Node) -> gleam@set:contains(Container_nodes, Node) end
),
case All_nodes_exist of
false ->
false;
true ->
gleam@list:all(
Potential_nodes,
fun(Src) ->
Potential_successors = yog@model:successors(Potential, Src),
gleam@list:all(
Potential_successors,
fun(Edge) ->
{Dst, _} = Edge,
edge_exists_in(Container, Src, Dst)
end
)
end
)
end.
-file("src/yog/operation.gleam", 519).
?DOC(" Filters edges to only those that exist in both graphs.\n").
-spec filter_edges_by_existence(
yog@model:graph(UKM, UKN),
yog@model:graph(UKM, UKN)
) -> yog@model:graph(UKM, UKN).
filter_edges_by_existence(First, Second) ->
First_nodes = yog@model:all_nodes(First),
gleam@list:fold(
First_nodes,
First,
fun(G, Src) ->
Successors = yog@model:successors(G, Src),
gleam@list:fold(
Successors,
G,
fun(Acc_g, Edge) ->
{Dst, _} = Edge,
case edge_exists_in(Second, Src, Dst) of
true ->
Acc_g;
false ->
yog@model:remove_edge(Acc_g, Src, Dst)
end
end
)
end
).
-file("src/yog/operation.gleam", 71).
?DOC(" Returns a graph containing only nodes and edges that exist in both input graphs.\n").
-spec intersection(yog@model:graph(UFM, UFN), yog@model:graph(UFM, UFN)) -> yog@model:graph(UFM, UFN).
intersection(First, Second) ->
First_nodes = gleam@set:from_list(yog@model:all_nodes(First)),
Second_nodes = gleam@set:from_list(yog@model:all_nodes(Second)),
Common_nodes = gleam@set:intersection(First_nodes, Second_nodes),
Filtered_first = begin
_pipe = yog@transform:filter_nodes(First, fun(_) -> true end),
filter_nodes_by_set(_pipe, Common_nodes)
end,
filter_edges_by_existence(Filtered_first, Second).
-file("src/yog/operation.gleam", 538).
?DOC(" Filters out edges that exist in the second graph.\n").
-spec filter_edges_not_in(yog@model:graph(UKU, UKV), yog@model:graph(UKU, UKV)) -> yog@model:graph(UKU, UKV).
filter_edges_not_in(First, Second) ->
First_nodes = yog@model:all_nodes(First),
gleam@list:fold(
First_nodes,
First,
fun(G, Src) ->
Successors = yog@model:successors(G, Src),
gleam@list:fold(
Successors,
G,
fun(Acc_g, Edge) ->
{Dst, _} = Edge,
case edge_exists_in(Second, Src, Dst) of
true ->
yog@model:remove_edge(Acc_g, Src, Dst);
false ->
Acc_g
end
end
)
end
).
-file("src/yog/operation.gleam", 85).
?DOC(
" Returns a graph containing nodes and edges that exist in the first graph\n"
" but not in the second.\n"
).
-spec difference(yog@model:graph(UFU, UFV), yog@model:graph(UFU, UFV)) -> yog@model:graph(UFU, UFV).
difference(First, Second) ->
First_nodes = gleam@set:from_list(yog@model:all_nodes(First)),
Second_nodes = gleam@set:from_list(yog@model:all_nodes(Second)),
Nodes_only_in_first = gleam@set:difference(First_nodes, Second_nodes),
Without_common_edges = filter_edges_not_in(First, Second),
Nodes_common = gleam@set:intersection(First_nodes, Second_nodes),
Nodes_to_check = gleam@set:to_list(Nodes_common),
gleam@list:fold(
Nodes_to_check,
Without_common_edges,
fun(G, Node) ->
Has_edges = not gleam@list:is_empty(
yog@model:successor_ids(G, Node)
)
orelse not gleam@list:is_empty(
begin
_pipe = yog@model:predecessors(G, Node),
gleam@list:map(_pipe, fun(T) -> erlang:element(1, T) end)
end
),
case Has_edges of
true ->
G;
false ->
case gleam@set:contains(Nodes_only_in_first, Node) of
true ->
G;
false ->
yog@model:remove_node(G, Node)
end
end
end
).
-file("src/yog/operation.gleam", 112).
?DOC(" Returns a graph containing edges that exist in exactly one of the input graphs.\n").
-spec symmetric_difference(yog@model:graph(UGC, UGD), yog@model:graph(UGC, UGD)) -> yog@model:graph(UGC, UGD).
symmetric_difference(First, Second) ->
First_only = difference(First, Second),
Second_only = difference(Second, First),
union(First_only, Second_only).