From 0ab3e33d7575656584c90ee90846b02602862429 Mon Sep 17 00:00:00 2001 From: Saurav Bhattacharya Date: Sat, 14 Mar 2026 04:34:40 -0700 Subject: [PATCH] refactor: extract DirectedAdj and neighborsOf into GraphUtils - 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 --- Gvisual/src/gvisual/CycleAnalyzer.java | 3 +- .../src/gvisual/GraphColoringAnalyzer.java | 33 ++++----- Gvisual/src/gvisual/GraphEntropyAnalyzer.java | 7 +- Gvisual/src/gvisual/GraphUtils.java | 72 +++++++++++++++++++ .../src/gvisual/TopologicalSortAnalyzer.java | 48 ++----------- 5 files changed, 96 insertions(+), 67 deletions(-) diff --git a/Gvisual/src/gvisual/CycleAnalyzer.java b/Gvisual/src/gvisual/CycleAnalyzer.java index c9a2379..40379bc 100644 --- a/Gvisual/src/gvisual/CycleAnalyzer.java +++ b/Gvisual/src/gvisual/CycleAnalyzer.java @@ -661,8 +661,7 @@ private Iterable getSuccessors(String v) { } private Iterable getNeighbors(String v) { - Collection nbrs = graph.getNeighbors(v); - return nbrs != null ? nbrs : Collections.emptyList(); + return GraphUtils.neighborsOf(graph, v); } private String edgeKey(String v1, String v2) { diff --git a/Gvisual/src/gvisual/GraphColoringAnalyzer.java b/Gvisual/src/gvisual/GraphColoringAnalyzer.java index abb7293..5747dbd 100644 --- a/Gvisual/src/gvisual/GraphColoringAnalyzer.java +++ b/Gvisual/src/gvisual/GraphColoringAnalyzer.java @@ -180,13 +180,11 @@ public ColoringResult computeDSatur() { // Assign smallest available color Set usedColors = new HashSet<>(); - Collection neighbors = graph.getNeighbors(best); - if (neighbors != null) { - for (String neighbor : neighbors) { - Integer nc = colorAssignment.get(neighbor); - if (nc != null) { - usedColors.add(nc); - } + Collection neighbors = GraphUtils.neighborsOf(graph, best); + for (String neighbor : neighbors) { + Integer nc = colorAssignment.get(neighbor); + if (nc != null) { + usedColors.add(nc); } } @@ -242,10 +240,8 @@ public int chromaticLowerBound() { // Sort candidates by degree descending for better heuristic List candidates = new ArrayList<>(); - Collection neighbors = graph.getNeighbors(start); - if (neighbors != null) { - candidates.addAll(neighbors); - } + Collection neighbors = GraphUtils.neighborsOf(graph, start); + candidates.addAll(neighbors); candidates.sort((a, b) -> Integer.compare(graph.degree(b), graph.degree(a))); for (String candidate : candidates) { @@ -359,8 +355,7 @@ private boolean backtrackColor(List vertices, int idx, int k, private boolean canAssign(String vertex, int color, Map assignment) { - Collection neighbors = graph.getNeighbors(vertex); - if (neighbors == null) return true; + Collection neighbors = GraphUtils.neighborsOf(graph, vertex); for (String neighbor : neighbors) { Integer nc = assignment.get(neighbor); if (nc != null && nc == color) { @@ -637,13 +632,11 @@ private ColoringResult greedyColor(List vertexOrder) { for (String vertex : vertexOrder) { Set usedColors = new HashSet<>(); - Collection neighbors = graph.getNeighbors(vertex); - if (neighbors != null) { - for (String neighbor : neighbors) { - Integer neighborColor = colorAssignment.get(neighbor); - if (neighborColor != null) { - usedColors.add(neighborColor); - } + Collection neighbors = GraphUtils.neighborsOf(graph, vertex); + for (String neighbor : neighbors) { + Integer neighborColor = colorAssignment.get(neighbor); + if (neighborColor != null) { + usedColors.add(neighborColor); } } diff --git a/Gvisual/src/gvisual/GraphEntropyAnalyzer.java b/Gvisual/src/gvisual/GraphEntropyAnalyzer.java index 8c2c757..7104449 100644 --- a/Gvisual/src/gvisual/GraphEntropyAnalyzer.java +++ b/Gvisual/src/gvisual/GraphEntropyAnalyzer.java @@ -225,7 +225,7 @@ private void computeNeighbourhoodEntropy() { double sum = 0; for (String v : graph.getVertices()) { - Collection nbrs = graph.getNeighbors(v); + Collection nbrs = GraphUtils.neighborsOf(graph, v); if (nbrs == null || nbrs.isEmpty()) { neighbourhoodEntropy.put(v, 0.0); continue; @@ -323,7 +323,7 @@ private void computeChromaticEntropy() { Map colors = new HashMap<>(); for (String v : vertices) { Set usedColors = new HashSet<>(); - Collection nbrs = graph.getNeighbors(v); + Collection nbrs = GraphUtils.neighborsOf(graph, v); if (nbrs != null) { for (String u : nbrs) { Integer c = colors.get(u); @@ -452,8 +452,7 @@ private static double logFactorial(int n) { * C(v) = 2T / (d(v)(d(v)-1)) where T is the number of triangles. */ private double localClusteringCoefficient(String v) { - Collection nbrs = graph.getNeighbors(v); - if (nbrs == null) return 0; + Collection nbrs = GraphUtils.neighborsOf(graph, v); List nbrList = new ArrayList<>(nbrs); int d = nbrList.size(); if (d < 2) return 0; diff --git a/Gvisual/src/gvisual/GraphUtils.java b/Gvisual/src/gvisual/GraphUtils.java index 0612318..0622f95 100644 --- a/Gvisual/src/gvisual/GraphUtils.java +++ b/Gvisual/src/gvisual/GraphUtils.java @@ -648,4 +648,76 @@ public static List reconstructPath( Collections.reverse(path); return path; } + + // ── Null-safe neighbor access ─────────────────────────────── + + /** + * Returns the neighbors of a vertex, never {@code null}. + * Wraps {@code graph.getNeighbors(v)} with a null-safe fallback. + * + * @param graph the JUNG graph + * @param v the vertex + * @return neighbors of v, or an empty collection if null + */ + public static Collection neighborsOf( + Graph graph, String v) { + Collection nbrs = graph.getNeighbors(v); + return nbrs != null ? nbrs : Collections.emptyList(); + } + + // ── Directed adjacency ────────────────────────────────────── + + /** + * Directed adjacency structure: vertices with successor and predecessor + * maps. Extracted from {@link TopologicalSortAnalyzer} for reuse by + * any analyzer that needs directed-edge traversal. + */ + public static final class DirectedAdj { + /** All vertices in the graph. */ + public final Set vertices; + /** Vertex → set of outgoing neighbors (vertex1 → vertex2). */ + public final Map> successors; + /** Vertex → set of incoming neighbors. */ + public final Map> predecessors; + + public DirectedAdj(Set vertices, + Map> successors, + Map> predecessors) { + this.vertices = vertices; + this.successors = successors; + this.predecessors = predecessors; + } + } + + /** + * Builds directed adjacency maps from a graph. Each edge is interpreted + * as vertex1 → vertex2. + * + * @param graph the JUNG graph + * @return a {@link DirectedAdj} with successor and predecessor maps + */ + public static DirectedAdj buildDirectedAdjacencyMap( + Graph graph) { + Map> successors = new HashMap>(); + Map> predecessors = new HashMap>(); + Set allVertices = new HashSet(); + + for (String v : graph.getVertices()) { + allVertices.add(v); + successors.put(v, new HashSet()); + predecessors.put(v, new HashSet()); + } + + for (edge e : graph.getEdges()) { + String from = e.getVertex1(); + String to = e.getVertex2(); + if (from != null && to != null + && allVertices.contains(from) && allVertices.contains(to)) { + successors.get(from).add(to); + predecessors.get(to).add(from); + } + } + + return new DirectedAdj(allVertices, successors, predecessors); + } } diff --git a/Gvisual/src/gvisual/TopologicalSortAnalyzer.java b/Gvisual/src/gvisual/TopologicalSortAnalyzer.java index 7c14836..3574df4 100644 --- a/Gvisual/src/gvisual/TopologicalSortAnalyzer.java +++ b/Gvisual/src/gvisual/TopologicalSortAnalyzer.java @@ -183,46 +183,12 @@ public VertexDependencyInfo(String vertex, Set allDependencies, * {@link #analyze()}, {@link #analyzeDependencies(String)}, and * {@link #countChoicePoints()}. */ - private static class DirectedAdj { - final Set vertices; - final Map> successors; - final Map> predecessors; - - DirectedAdj(Set vertices, - Map> successors, - Map> predecessors) { - this.vertices = vertices; - this.successors = successors; - this.predecessors = predecessors; - } - } - /** - * Builds the directed adjacency maps from the graph. Each edge is - * interpreted as vertex1 → vertex2 (vertex1 must come before vertex2). + * Delegates to {@link GraphUtils.DirectedAdj} — the shared directed + * adjacency builder extracted from this class. */ - private DirectedAdj buildDirectedAdj() { - Map> successors = new HashMap>(); - Map> predecessors = new HashMap>(); - Set allVertices = new HashSet(); - - for (String v : graph.getVertices()) { - allVertices.add(v); - successors.put(v, new HashSet()); - predecessors.put(v, new HashSet()); - } - - for (edge e : graph.getEdges()) { - String from = e.getVertex1(); - String to = e.getVertex2(); - if (from != null && to != null - && allVertices.contains(from) && allVertices.contains(to)) { - successors.get(from).add(to); - predecessors.get(to).add(from); - } - } - - return new DirectedAdj(allVertices, successors, predecessors); + private GraphUtils.DirectedAdj buildDirectedAdj() { + return GraphUtils.buildDirectedAdjacencyMap(graph); } // ── Core algorithms ───────────────────────────────────────── @@ -240,7 +206,7 @@ private DirectedAdj buildDirectedAdj() { * @return complete topological sort analysis */ public TopologicalSortResult analyze() { - DirectedAdj adj = buildDirectedAdj(); + GraphUtils.DirectedAdj adj = buildDirectedAdj(); Map> successors = adj.successors; Map> predecessors = adj.predecessors; Set allVertices = adj.vertices; @@ -351,7 +317,7 @@ public VertexDependencyInfo analyzeDependencies(String vertex) { return null; } - DirectedAdj adj = buildDirectedAdj(); + GraphUtils.DirectedAdj adj = buildDirectedAdj(); Map> successors = adj.successors; Map> predecessors = adj.predecessors; @@ -416,7 +382,7 @@ public int countChoicePoints() { // Re-use shared adjacency builder and count points where // multiple vertices are ready simultaneously - DirectedAdj adj = buildDirectedAdj(); + GraphUtils.DirectedAdj adj = buildDirectedAdj(); Map> successors = adj.successors; Map inDegree = new HashMap();