Skip to content

Commit

Permalink
Introduce AbstractGraph. This is a bare-bones base class that all Gra…
Browse files Browse the repository at this point in the history
…ph implementations should be able to extend (analogous to e.g. AbstractSet for Sets). It ensures there is consistent equals()/hashCode() across different Graph implementations. Additionally, this CL drops the requirement that two graphs have to have "the same type" to be equal.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=114901380
  • Loading branch information
Bezier89 authored and cpovirk committed Feb 17, 2016
1 parent 529027a commit 2007886
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 234 deletions.
35 changes: 29 additions & 6 deletions guava-tests/test/com/google/common/graph/GraphEqualsTest.java
Expand Up @@ -36,6 +36,7 @@ public final class GraphEqualsTest {
private static final Integer N2 = 2;
private static final Integer N3 = 3;

private static final String E11 = "1-1";
private static final String E12 = "1-2";
private static final String E12_A = "1-2a";
private static final String E13 = "1-3";
Expand Down Expand Up @@ -96,12 +97,11 @@ public void equals_edgeSetsDiffer() {
new EqualsTester().addEqualityGroup(graph).addEqualityGroup(g2).testEquals();
}

// Node/edge sets are the same, but types differ.
// Node/edge sets are the same, but node/edge connections differ due to graph type.
@Test
public void equals_typesDiffer() {
public void equals_directedVsUndirected() {
graph.addEdge(E12, N1, N2);

// Whatever graphType specifies, pick another type.
Graph<Integer, String> g2;
switch (graphType) {
case UNDIRECTED:
Expand All @@ -119,7 +119,30 @@ public void equals_typesDiffer() {
new EqualsTester().addEqualityGroup(graph).addEqualityGroup(g2).testEquals();
}

// Node/edge sets and graph type are the same, but node/edge connections differ.
// Node/edge sets and node/edge connections are the same, but types differ.
// (In this case the graphs are considered equal; the type differences are irrelevant.)
@Test
public void equals_selfLoop_directedVsUndirected() {
graph.addEdge(E11, N1, N1);

Graph<Integer, String> g2;
switch (graphType) {
case UNDIRECTED:
g2 = Graphs.createDirected();
break;
case DIRECTED:
g2 = Graphs.createUndirected();
break;
default:
throw new IllegalStateException("Unexpected graph type: " + graphType);
}

g2.addEdge(E11, N1, N1);

new EqualsTester().addEqualityGroup(graph, g2).testEquals();
}

// Node/edge sets are the same, but node/edge connections differ.
@Test
public void equals_connectionsDiffer() {
graph.addEdge(E12, N1, N2);
Expand All @@ -133,7 +156,7 @@ public void equals_connectionsDiffer() {
new EqualsTester().addEqualityGroup(graph).addEqualityGroup(g2).testEquals();
}

// Node/edge sets, graph type, and node/edge connections are the same, but GraphConfigs differ.
// Node/edge sets and node/edge connections are the same, but GraphConfigs differ.
// (In this case the graphs are considered equal; the config differences are irrelevant.)
@Test
public void equals_configsDiffer() {
Expand All @@ -145,7 +168,7 @@ public void equals_configsDiffer() {
new EqualsTester().addEqualityGroup(graph, g2).testEquals();
}

// Node/edge sets, graph type, and node/edge connections are the same, but edge order differs.
// Node/edge sets and node/edge connections are the same, but edge order differs.
// (In this case the graphs are considered equal; the edge add orderings are irrelevant.)
@Test
public void equals_edgeAddOrdersDiffer() {
Expand Down
79 changes: 79 additions & 0 deletions guava/src/com/google/common/graph/AbstractGraph.java
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2016 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.common.graph;

import javax.annotation.Nullable;

/**
* This class provides a skeletal implementation of {@link Graph}. It is recommended to extend this
* class rather than implement {@link Graph} directly, to ensure consistent {@link #equals(Object)}
* and {@link #hashCode()} results across different graph implementations.
*
* @author James Sexton
* @param <N> Node parameter type
* @param <E> Edge parameter type
*/
public abstract class AbstractGraph<N, E> implements Graph<N, E> {

protected final GraphConfig config;

/**
* Not all subclasses necessarily need to expose a constructor that takes a {@link GraphConfig},
* but they do need to provide the {@code config} here that will be returned by {@link #config()}.
*/
protected AbstractGraph(GraphConfig config) {
this.config = config;
}

@Override
public GraphConfig config() {
return config;
}

@Override
public long degree(Object node) {
return incidentEdges(node).size();
}

@Override
public long inDegree(Object node) {
return inEdges(node).size();
}

@Override
public long outDegree(Object node) {
return outEdges(node).size();
}

@Override
public boolean equals(@Nullable Object object) {
if (!(object instanceof Graph)) {
return false;
}
return Graphs.equal(this, (Graph<?, ?>) object);
}

@Override
public int hashCode() {
return Graphs.hashCode(this);
}

@Override
public String toString() {
return Graphs.toString(this);
}
}
Expand Up @@ -25,7 +25,11 @@
* @param <N> Node parameter type
* @param <E> Edge parameter type
*/
abstract class AbstractImmutableGraph<N, E> implements Graph<N, E> {
abstract class AbstractImmutableGraph<N, E> extends AbstractGraph<N, E> {

AbstractImmutableGraph(GraphConfig config) {
super(config);
}

@Override
public boolean addNode(N n) {
Expand Down
25 changes: 19 additions & 6 deletions guava/src/com/google/common/graph/Graph.java
Expand Up @@ -156,6 +156,8 @@
* </ol>
* Note that (1) and (2) are generally preferred. (5) is generally a hazardous design choice
* and should be avoided, because keeping the internal data structures consistent can be tricky.
* <li>Prefer extending {@link AbstractGraph} over implementing {@link Graph} directly. This will
* ensure consistent {@link #equals(Object)} and {@link #hashCode()} across implementations.
* <li>{@code Multimap}s are not sufficient internal data structures for Graph implementations
* that support isolated nodes (nodes that have no incident edges), due to their restriction
* that a key either maps to at least one value, or is not present in the {@code Multimap}.
Expand Down Expand Up @@ -393,19 +395,16 @@ public interface Graph<N, E> {
boolean removeEdge(Object edge);

/**
* Returns {@code true} iff {@code object} is the same type of graph (directed, undirected,
* hypergraph) as this graph, and the same node/edge relationships exist in both graphs.
* Returns {@code true} iff {@code object} is a graph that has the same node/edge relationships
* as those in this graph.
*
* <p>Thus, two graphs A and B are equal if <b>all</b> of the following are true:
* <ul>
* <li>A and B are of the same type ({@code DirectedGraph, UndirectedGraph, Hypergraph})
* <li>A and B have the same node set
* <li>A and B have the same edge set
* <li>A and B have the same incidence relationships, e.g., for each node/edge in A and in B
* its incident edge/node set in A is the same as its incident edge/node set in B.
* <br>Thus, even if a {@code node} has the same sets of <i>adjacent</i> nodes
* (neighbors) in both A and B, if the sets of edges by which {@code node} is connected to
* its adjacent nodes are not the same in both A and B, then A and B are not equal.
* <br>Thus, every edge in A and B connect the same nodes in the same direction (if any).
* </ul>
*
* <p>Properties that are <b>not</b> respected by this method:
Expand All @@ -416,7 +415,21 @@ public interface Graph<N, E> {
* <li>Edge/node ordering. The order in which edges or nodes are added to the graph, and the
* order in which they are iterated over, are irrelevant.
* </ul>
*
* <p>A reference implementation of this is provided by {@link Graphs#equal(Graph, Graph)}.
*/
@Override
boolean equals(@Nullable Object object);

/**
* Returns the hash code for this graph. The hash code of a graph is defined as the hash code
* of a map from each of the graph's nodes to their incident edges.
*
* <p>A reference implementation of this is provided by {@link Graphs#hashCode(Graph)}.
*
* <p>Note that by this definition, two graphs that are equal in every aspect except edge
* direction will have the same hash code (but can still be differentiated by {@link #equals}.
*/
@Override
int hashCode();
}
113 changes: 60 additions & 53 deletions guava/src/com/google/common/graph/Graphs.java
Expand Up @@ -29,6 +29,7 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nullable;
Expand Down Expand Up @@ -379,13 +380,11 @@ public static <N, E> UndirectedGraph<N, E> createUndirected(GraphConfig config)
}

/**
* Returns true iff {@code graph1} and {@code graph2} have the same node and edge sets and
* each edge has the same source and target in both graphs.
* Returns true iff {@code graph1} and {@code graph2} have the same node/edge relationships.
*
* @see Graph#equals(Object)
*/
public static <N, E> boolean equal(
@Nullable DirectedGraph<?, ?> graph1, @Nullable DirectedGraph<?, ?> graph2) {
public static boolean equal(@Nullable Graph<?, ?> graph1, @Nullable Graph<?, ?> graph2) {
if (graph1 == graph2) {
return true;
}
Expand All @@ -394,76 +393,45 @@ public static <N, E> boolean equal(
return false;
}

if (!graph1.nodes().equals(graph2.nodes()) || !graph1.edges().equals(graph2.edges())) {
if (graph1.edges().size() != graph2.edges().size()) {
return false;
}

for (Object edge : graph1.edges()) {
if (!graph1.source(edge).equals(graph2.source(edge))
|| !graph1.target(edge).equals(graph2.target(edge))) {
return false;
}
}
return true;
}

/**
* Returns true iff {@code graph1} and {@code graph2} have the same node and edge sets and
* each edge has the same incident node set in both graphs.
*
* @see Graph#equals(Object)
*/
public static boolean equal(@Nullable Graph<?, ?> graph1, @Nullable Graph<?, ?> graph2) {
if (graph1 == graph2) {
return true;
}

if (graph1 == null || graph2 == null) {
if (!graph1.nodes().equals(graph2.nodes())) {
return false;
}

if (!graph1.nodes().equals(graph2.nodes()) || !graph1.edges().equals(graph2.edges())) {
return false;
}

for (Object edge : graph1.edges()) {
if (!graph1.incidentNodes(edge).equals(graph2.incidentNodes(edge))) {
for (Object node : graph1.nodes()) {
if (!graph1.inEdges(node).equals(graph2.inEdges(node))) {
return false;
}
// TODO(b/27195992): Consider an optimization for the case where both graphs are undirected.
if (!graph1.outEdges(node).equals(graph2.outEdges(node))) {
return false;
}
}

return true;
}

/**
* Returns a string representation of {@code graph}, encoding the direction of each edge.
* Returns the hash code of {@code graph}.
*
* @see Graph#hashCode()
*/
public static String toString(final DirectedGraph<?, ?> graph) {
Function<Object, String> edgeToEndpoints = new Function<Object, String>() {
@Override
public String apply(Object edge) {
return String.format("<%s -> %s>", graph.source(edge), graph.target(edge));
}
};
return String.format("config: %s, nodes: %s, edges: %s",
graph.config(),
graph.nodes(),
Maps.asMap(graph.edges(), edgeToEndpoints));
public static int hashCode(Graph<?, ?> graph) {
return nodeToIncidentEdges(graph).hashCode();
}

/**
* Returns a string representation of {@code graph}, without regard to direction of edges.
* Returns a string representation of {@code graph}. Encodes edge direction if {@code graph}
* is a {@link DirectedGraph}.
*/
public static String toString(final Graph<?, ?> graph) {
Function<Object, String> edgeToIncidentNodes = new Function<Object, String>() {
@Override
public String apply(Object edge) {
return graph.incidentNodes(edge).toString();
}
};
public static String toString(Graph<?, ?> graph) {
return String.format("config: %s, nodes: %s, edges: %s",
graph.config(),
graph.nodes(),
Maps.asMap(graph.edges(), edgeToIncidentNodes));
Maps.asMap(graph.edges(), edgeToIncidentNodesString(graph)));
}

/**
Expand All @@ -481,4 +449,43 @@ public boolean apply(E edge) {
}
};
}

/**
* Returns a map that is a live view of {@code graph}, with nodes as keys
* and the set of incident edges as values.
*/
private static <N, E> Map<N, Set<E>> nodeToIncidentEdges(final Graph<N, E> graph) {
checkNotNull(graph, "graph");
return Maps.asMap(graph.nodes(), new Function<N, Set<E>>() {
@Override
public Set<E> apply(N node) {
return graph.incidentEdges(node);
}
});
}

/**
* Returns a function that transforms an edge into a string representation of its incident nodes
* in {@code graph}. The function's {@code apply} method will throw an
* {@link IllegalArgumentException} if {@code graph} does not contain {@code edge}.
*/
private static Function<Object, String> edgeToIncidentNodesString(final Graph<?, ?> graph) {
if (graph instanceof DirectedGraph) {
@SuppressWarnings("unchecked")
final DirectedGraph<?, ?> directedGraph = (DirectedGraph<?, ?>) graph;
return new Function<Object, String>() {
@Override
public String apply(Object edge) {
return String.format("<%s -> %s>",
directedGraph.source(edge), directedGraph.target(edge));
}
};
}
return new Function<Object, String>() {
@Override
public String apply(Object edge) {
return graph.incidentNodes(edge).toString();
}
};
}
}

0 comments on commit 2007886

Please sign in to comment.