Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit eb060a0

Browse files
refactor: consolidate adjacency/BFS into GraphUtils (#75)
- Move DirectedAdj class and buildDirectedAdjacencyMap from TopologicalSortAnalyzer to GraphUtils for reuse by other analyzers - Add GraphUtils.neighborsOf() null-safe neighbor access helper - Update CycleAnalyzer, GraphColoringAnalyzer, GraphEntropyAnalyzer to use shared neighborsOf() instead of inline null checks - Remove ~67 lines of duplicated code across 4 analyzers Closes #43
1 parent 5461013 commit eb060a0

5 files changed

Lines changed: 96 additions & 67 deletions

File tree

Gvisual/src/gvisual/CycleAnalyzer.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -661,8 +661,7 @@ private Iterable<String> getSuccessors(String v) {
661661
}
662662

663663
private Iterable<String> getNeighbors(String v) {
664-
Collection<String> nbrs = graph.getNeighbors(v);
665-
return nbrs != null ? nbrs : Collections.<String>emptyList();
664+
return GraphUtils.neighborsOf(graph, v);
666665
}
667666

668667
private String edgeKey(String v1, String v2) {

Gvisual/src/gvisual/GraphColoringAnalyzer.java

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,11 @@ public ColoringResult computeDSatur() {
180180

181181
// Assign smallest available color
182182
Set<Integer> usedColors = new HashSet<>();
183-
Collection<String> neighbors = graph.getNeighbors(best);
184-
if (neighbors != null) {
185-
for (String neighbor : neighbors) {
186-
Integer nc = colorAssignment.get(neighbor);
187-
if (nc != null) {
188-
usedColors.add(nc);
189-
}
183+
Collection<String> neighbors = GraphUtils.neighborsOf(graph, best);
184+
for (String neighbor : neighbors) {
185+
Integer nc = colorAssignment.get(neighbor);
186+
if (nc != null) {
187+
usedColors.add(nc);
190188
}
191189
}
192190

@@ -242,10 +240,8 @@ public int chromaticLowerBound() {
242240

243241
// Sort candidates by degree descending for better heuristic
244242
List<String> candidates = new ArrayList<>();
245-
Collection<String> neighbors = graph.getNeighbors(start);
246-
if (neighbors != null) {
247-
candidates.addAll(neighbors);
248-
}
243+
Collection<String> neighbors = GraphUtils.neighborsOf(graph, start);
244+
candidates.addAll(neighbors);
249245
candidates.sort((a, b) -> Integer.compare(graph.degree(b), graph.degree(a)));
250246

251247
for (String candidate : candidates) {
@@ -359,8 +355,7 @@ private boolean backtrackColor(List<String> vertices, int idx, int k,
359355

360356
private boolean canAssign(String vertex, int color,
361357
Map<String, Integer> assignment) {
362-
Collection<String> neighbors = graph.getNeighbors(vertex);
363-
if (neighbors == null) return true;
358+
Collection<String> neighbors = GraphUtils.neighborsOf(graph, vertex);
364359
for (String neighbor : neighbors) {
365360
Integer nc = assignment.get(neighbor);
366361
if (nc != null && nc == color) {
@@ -637,13 +632,11 @@ private ColoringResult greedyColor(List<String> vertexOrder) {
637632

638633
for (String vertex : vertexOrder) {
639634
Set<Integer> usedColors = new HashSet<>();
640-
Collection<String> neighbors = graph.getNeighbors(vertex);
641-
if (neighbors != null) {
642-
for (String neighbor : neighbors) {
643-
Integer neighborColor = colorAssignment.get(neighbor);
644-
if (neighborColor != null) {
645-
usedColors.add(neighborColor);
646-
}
635+
Collection<String> neighbors = GraphUtils.neighborsOf(graph, vertex);
636+
for (String neighbor : neighbors) {
637+
Integer neighborColor = colorAssignment.get(neighbor);
638+
if (neighborColor != null) {
639+
usedColors.add(neighborColor);
647640
}
648641
}
649642

Gvisual/src/gvisual/GraphEntropyAnalyzer.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ private void computeNeighbourhoodEntropy() {
225225

226226
double sum = 0;
227227
for (String v : graph.getVertices()) {
228-
Collection<String> nbrs = graph.getNeighbors(v);
228+
Collection<String> nbrs = GraphUtils.neighborsOf(graph, v);
229229
if (nbrs == null || nbrs.isEmpty()) {
230230
neighbourhoodEntropy.put(v, 0.0);
231231
continue;
@@ -323,7 +323,7 @@ private void computeChromaticEntropy() {
323323
Map<String, Integer> colors = new HashMap<>();
324324
for (String v : vertices) {
325325
Set<Integer> usedColors = new HashSet<>();
326-
Collection<String> nbrs = graph.getNeighbors(v);
326+
Collection<String> nbrs = GraphUtils.neighborsOf(graph, v);
327327
if (nbrs != null) {
328328
for (String u : nbrs) {
329329
Integer c = colors.get(u);
@@ -452,8 +452,7 @@ private static double logFactorial(int n) {
452452
* C(v) = 2T / (d(v)(d(v)-1)) where T is the number of triangles.
453453
*/
454454
private double localClusteringCoefficient(String v) {
455-
Collection<String> nbrs = graph.getNeighbors(v);
456-
if (nbrs == null) return 0;
455+
Collection<String> nbrs = GraphUtils.neighborsOf(graph, v);
457456
List<String> nbrList = new ArrayList<>(nbrs);
458457
int d = nbrList.size();
459458
if (d < 2) return 0;

Gvisual/src/gvisual/GraphUtils.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,4 +648,76 @@ public static List<String> reconstructPath(
648648
Collections.reverse(path);
649649
return path;
650650
}
651+
652+
// ── Null-safe neighbor access ───────────────────────────────
653+
654+
/**
655+
* Returns the neighbors of a vertex, never {@code null}.
656+
* Wraps {@code graph.getNeighbors(v)} with a null-safe fallback.
657+
*
658+
* @param graph the JUNG graph
659+
* @param v the vertex
660+
* @return neighbors of v, or an empty collection if null
661+
*/
662+
public static Collection<String> neighborsOf(
663+
Graph<String, edge> graph, String v) {
664+
Collection<String> nbrs = graph.getNeighbors(v);
665+
return nbrs != null ? nbrs : Collections.<String>emptyList();
666+
}
667+
668+
// ── Directed adjacency ──────────────────────────────────────
669+
670+
/**
671+
* Directed adjacency structure: vertices with successor and predecessor
672+
* maps. Extracted from {@link TopologicalSortAnalyzer} for reuse by
673+
* any analyzer that needs directed-edge traversal.
674+
*/
675+
public static final class DirectedAdj {
676+
/** All vertices in the graph. */
677+
public final Set<String> vertices;
678+
/** Vertex → set of outgoing neighbors (vertex1 → vertex2). */
679+
public final Map<String, Set<String>> successors;
680+
/** Vertex → set of incoming neighbors. */
681+
public final Map<String, Set<String>> predecessors;
682+
683+
public DirectedAdj(Set<String> vertices,
684+
Map<String, Set<String>> successors,
685+
Map<String, Set<String>> predecessors) {
686+
this.vertices = vertices;
687+
this.successors = successors;
688+
this.predecessors = predecessors;
689+
}
690+
}
691+
692+
/**
693+
* Builds directed adjacency maps from a graph. Each edge is interpreted
694+
* as vertex1 → vertex2.
695+
*
696+
* @param graph the JUNG graph
697+
* @return a {@link DirectedAdj} with successor and predecessor maps
698+
*/
699+
public static DirectedAdj buildDirectedAdjacencyMap(
700+
Graph<String, edge> graph) {
701+
Map<String, Set<String>> successors = new HashMap<String, Set<String>>();
702+
Map<String, Set<String>> predecessors = new HashMap<String, Set<String>>();
703+
Set<String> allVertices = new HashSet<String>();
704+
705+
for (String v : graph.getVertices()) {
706+
allVertices.add(v);
707+
successors.put(v, new HashSet<String>());
708+
predecessors.put(v, new HashSet<String>());
709+
}
710+
711+
for (edge e : graph.getEdges()) {
712+
String from = e.getVertex1();
713+
String to = e.getVertex2();
714+
if (from != null && to != null
715+
&& allVertices.contains(from) && allVertices.contains(to)) {
716+
successors.get(from).add(to);
717+
predecessors.get(to).add(from);
718+
}
719+
}
720+
721+
return new DirectedAdj(allVertices, successors, predecessors);
722+
}
651723
}

Gvisual/src/gvisual/TopologicalSortAnalyzer.java

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -183,46 +183,12 @@ public VertexDependencyInfo(String vertex, Set<String> allDependencies,
183183
* {@link #analyze()}, {@link #analyzeDependencies(String)}, and
184184
* {@link #countChoicePoints()}.
185185
*/
186-
private static class DirectedAdj {
187-
final Set<String> vertices;
188-
final Map<String, Set<String>> successors;
189-
final Map<String, Set<String>> predecessors;
190-
191-
DirectedAdj(Set<String> vertices,
192-
Map<String, Set<String>> successors,
193-
Map<String, Set<String>> predecessors) {
194-
this.vertices = vertices;
195-
this.successors = successors;
196-
this.predecessors = predecessors;
197-
}
198-
}
199-
200186
/**
201-
* Builds the directed adjacency maps from the graph. Each edge is
202-
* interpreted as vertex1 → vertex2 (vertex1 must come before vertex2).
187+
* Delegates to {@link GraphUtils.DirectedAdj} — the shared directed
188+
* adjacency builder extracted from this class.
203189
*/
204-
private DirectedAdj buildDirectedAdj() {
205-
Map<String, Set<String>> successors = new HashMap<String, Set<String>>();
206-
Map<String, Set<String>> predecessors = new HashMap<String, Set<String>>();
207-
Set<String> allVertices = new HashSet<String>();
208-
209-
for (String v : graph.getVertices()) {
210-
allVertices.add(v);
211-
successors.put(v, new HashSet<String>());
212-
predecessors.put(v, new HashSet<String>());
213-
}
214-
215-
for (edge e : graph.getEdges()) {
216-
String from = e.getVertex1();
217-
String to = e.getVertex2();
218-
if (from != null && to != null
219-
&& allVertices.contains(from) && allVertices.contains(to)) {
220-
successors.get(from).add(to);
221-
predecessors.get(to).add(from);
222-
}
223-
}
224-
225-
return new DirectedAdj(allVertices, successors, predecessors);
190+
private GraphUtils.DirectedAdj buildDirectedAdj() {
191+
return GraphUtils.buildDirectedAdjacencyMap(graph);
226192
}
227193

228194
// ── Core algorithms ─────────────────────────────────────────
@@ -240,7 +206,7 @@ private DirectedAdj buildDirectedAdj() {
240206
* @return complete topological sort analysis
241207
*/
242208
public TopologicalSortResult analyze() {
243-
DirectedAdj adj = buildDirectedAdj();
209+
GraphUtils.DirectedAdj adj = buildDirectedAdj();
244210
Map<String, Set<String>> successors = adj.successors;
245211
Map<String, Set<String>> predecessors = adj.predecessors;
246212
Set<String> allVertices = adj.vertices;
@@ -351,7 +317,7 @@ public VertexDependencyInfo analyzeDependencies(String vertex) {
351317
return null;
352318
}
353319

354-
DirectedAdj adj = buildDirectedAdj();
320+
GraphUtils.DirectedAdj adj = buildDirectedAdj();
355321
Map<String, Set<String>> successors = adj.successors;
356322
Map<String, Set<String>> predecessors = adj.predecessors;
357323

@@ -416,7 +382,7 @@ public int countChoicePoints() {
416382

417383
// Re-use shared adjacency builder and count points where
418384
// multiple vertices are ready simultaneously
419-
DirectedAdj adj = buildDirectedAdj();
385+
GraphUtils.DirectedAdj adj = buildDirectedAdj();
420386
Map<String, Set<String>> successors = adj.successors;
421387
Map<String, Integer> inDegree = new HashMap<String, Integer>();
422388

0 commit comments

Comments
 (0)