diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6139a03..bec3688 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,7 @@ plugins { // Apply the application plugin to add support for building a CLI application in Java. application + checkstyle id("com.gradleup.shadow") version "9.0.0-beta12" } @@ -16,6 +17,13 @@ repositories { mavenCentral() } +sourceSets { main { java { srcDirs("src/main/java") } } } + +checkstyle { + toolVersion = "10.12.4" + configFile = file("./checkstyle.xml") +} + dependencies { // Use JUnit Jupiter for testing. testImplementation(libs.junit.jupiter) @@ -26,6 +34,8 @@ dependencies { implementation(libs.guava) implementation("org.jline:jline:3.29.0") + + compileOnly("com.github.spotbugs:spotbugs-annotations:3.1.3") } // Apply a specific Java toolchain to ease working on different environments. diff --git a/app/checkstyle.xml b/app/checkstyle.xml new file mode 100644 index 0000000..97e8e9e --- /dev/null +++ b/app/checkstyle.xml @@ -0,0 +1,447 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/fun/youthlic/Graph.java b/app/src/main/java/fun/youthlic/Graph.java index 6695169..92df719 100644 --- a/app/src/main/java/fun/youthlic/Graph.java +++ b/app/src/main/java/fun/youthlic/Graph.java @@ -11,314 +11,354 @@ import java.util.Optional; import java.util.PriorityQueue; import java.util.Random; import java.util.function.Function; -import java.util.stream.IntStream; +/** + * Core class for impl graph. + */ public class Graph { - public record EdgeRecord(String node1, String node2, int weight) { + private final Map nodes; + private Map pageRanks; + + /** + * Initial func of Graph. + * + * @param input input text + * @param d d of page rank + */ + public Graph(final String input, final double d) { + nodes = new HashMap<>(); + parse(input); + initRageRanks(d); + } + + /** + * Initial func of Graph with default d. + * + * @param input input text + */ + public Graph(final String input) { + nodes = new HashMap<>(); + parse(input); + initRageRanks(0.85); + } + + /** + * Find bridge words. + * + * @param word1 first word + * @param word2 second word + * @return list of bridge words + */ + public Optional> findBridgeWords(final String word1, final String word2) { + final Node node1 = nodes.get(word1.toLowerCase(Locale.ENGLISH)); + final Node node2 = nodes.get(word2.toLowerCase(Locale.ENGLISH)); + + if (node1 == null || node2 == null) { + return Optional.empty(); } - private static class Node { + final var bridgeWords = new ArrayList(); + for (final var node : node1.edges.keySet()) { + if (node.edges.containsKey(node2)) { + bridgeWords.add(node.id); + } + } + return Optional.of(bridgeWords); + } - String id; - Map edges; + /** + * Find the shortest path between two words. + * + * @param word1 first word + * @param word2 second word + * @return list of the words in the path + */ + public Optional> findShortestPath(final String word1, final String word2) { + final var startWord = word1.toLowerCase(Locale.ENGLISH); + final var endWord = word2.toLowerCase(Locale.ENGLISH); + final var path = new ArrayList(); - Node(final String id) { - this.id = id; - this.edges = new HashMap<>(); - } - - void addEdge(final Node target) { - edges.put(target, edges.getOrDefault(target, 0) + 1); - } - - List getDOTEdges(final Function callback) { - final var dotEdges = new ArrayList(); - for (final var entry : edges.entrySet()) { - final Node target = entry.getKey(); - final int weight = entry.getValue(); - dotEdges.add( - callback.apply(new EdgeRecord(id, target.id, weight)) + ";\n"); - } - return dotEdges; - } - - List getDOTEdges() { - final var dotEdges = new ArrayList(); - for (final var entry : edges.entrySet()) { - final Node target = entry.getKey(); - final int weight = entry.getValue(); - dotEdges.add( - String.format("%s -> %s [label=\"%d\"];\n", id, target.id, weight)); - } - return dotEdges; - } + if (!nodes.containsKey(startWord) || !nodes.containsKey(endWord)) { + return Optional.empty(); } - public static void main(final String[] args) { - final var g = new Graph( - "To explore strange new worlds, To seek out new life and new civilizations"); - final var words = g.findBridgeWords("team", "so"); - System.out.println(words); - final var path = g.findShortestPath("the", "shared"); - System.out.println(path); - System.out.println( - g.toDOT(r -> String.format( - "\"%s\" -> \"%s\" [label=\"%d\"%s]", - r.node1 + (path.contains(r.node1) ? "~" : ""), - r.node2 + (path.contains(r.node2) ? "~" : ""), - r.weight, - (path.contains(r.node1) && - path.contains(r.node2) && - IntStream.range(0, path.size() - 1).anyMatch( - i -> path.get(i).equals(r.node1) && path.get(i + 1).equals(r.node2))) - ? ",color=\"red\"" - : ""))); - System.out.println(g.computePageRank("new", 0.85)); - System.out.println(g.randomWalk()); + final Node startNode = nodes.get(startWord); + final Node endNode = nodes.get(endWord); + + if (startNode == endNode) { + path.add(startNode.id); + return Optional.of(path); } - private final Map nodes; + final var distances = new HashMap(); + final var previousNodes = new HashMap(); + final var queue = new PriorityQueue( + Comparator.comparingInt(node -> distances.getOrDefault(node, Integer.MAX_VALUE))); - private Map pageRanks; + for (final var node : nodes.values()) { + distances.put(node, Integer.MAX_VALUE); + } + distances.put(startNode, 0); + queue.add(startNode); - public Graph(final String input, final double d) { - nodes = new HashMap<>(); - parse(input); - initRageRanks(d); + while (!queue.isEmpty()) { + final Node current = queue.poll(); + for (final var edge : current.edges.entrySet()) { + final Node neighbor = edge.getKey(); + final int weight = edge.getValue(); + + final int newDist = distances.get(current) + weight; + if (newDist < distances.get(neighbor)) { + distances.put(neighbor, newDist); + previousNodes.put(neighbor, current); + queue.add(neighbor); + } + } } - public Graph(final String input) { - nodes = new HashMap<>(); - parse(input); - initRageRanks(0.85); + if (distances.get(endNode) == Integer.MAX_VALUE) { + return Optional.of(path); } - public Optional> findBridgeWords(final String word1, final String word2) { - final Node node1 = nodes.get(word1.toLowerCase(Locale.ENGLISH)); - final Node node2 = nodes.get(word2.toLowerCase(Locale.ENGLISH)); - - if (node1 == null || node2 == null) { - return Optional.empty(); - } - - final var bridgeWords = new ArrayList(); - for (final var node : node1.edges.keySet()) { - if (node.edges.containsKey(node2)) { - bridgeWords.add(node.id); - } - } - return Optional.of(bridgeWords); + final var reversePath = new LinkedList(); + Node current = endNode; + while (current != null) { + reversePath.addFirst(current.id); + current = previousNodes.get(current); } - public List findShortestPath(final String word1, final String word2) { - final var startWord = word1.toLowerCase(Locale.ENGLISH); - final var endWord = word2.toLowerCase(Locale.ENGLISH); - final var path = new ArrayList(); + return Optional.of(reversePath); + } - if (!nodes.containsKey(startWord) || !nodes.containsKey(endWord)) { - return path; - } - - final Node startNode = nodes.get(startWord); - final Node endNode = nodes.get(endWord); - - if (startNode == endNode) { - path.add(startNode.id); - return path; - } - - final var distances = new HashMap(); - final var previousNodes = new HashMap(); - final var queue = new PriorityQueue( - Comparator.comparingInt(node -> distances.getOrDefault(node, Integer.MAX_VALUE))); - - for (final var node : nodes.values()) { - distances.put(node, Integer.MAX_VALUE); - } - distances.put(startNode, 0); - queue.add(startNode); - - while (!queue.isEmpty()) { - final Node current = queue.poll(); - for (final var edge : current.edges.entrySet()) { - final Node neighbor = edge.getKey(); - final int weight = edge.getValue(); - - final int newDist = distances.get(current) + weight; - if (newDist < distances.get(neighbor)) { - distances.put(neighbor, newDist); - previousNodes.put(neighbor, current); - queue.add(neighbor); - } - } - } - - if (distances.get(endNode) == Integer.MAX_VALUE) { - return path; - } - - final var reversePath = new LinkedList(); - Node current = endNode; - while (current != null) { - reversePath.addFirst(current.id); - current = previousNodes.get(current); - } - - return reversePath; + /** + * Calculate rage rank. + * + * @param nodeId id of node + * @return page rank + */ + public double computePageRank(final String nodeId) { + final Node node = nodes.get(nodeId); + if (node == null) { + return -1.0; } - public double computePageRank(final String nodeId, final double d) { - final Node node = nodes.get(nodeId); - if (node == null) { - return 0.0; - } + return pageRanks.getOrDefault(node, -1.0); + } - return pageRanks.getOrDefault(node, 0.0); + /** + * Random walk. + * + * @return list of words in the walk path + */ + @edu.umd.cs.findbugs.annotations.SuppressFBWarnings("PREDICTABLE_RANDOM") + public List randomWalk() { + final var path = new ArrayList(); + if (nodes.isEmpty()) { + return path; } - public List randomWalk() { - final var path = new ArrayList(); - if (nodes.isEmpty()) { - return path; + final var random = new Random(); + final var nodeIds = new ArrayList<>(nodes.keySet()); + final String startNodeId = nodeIds.get(random.nextInt(nodeIds.size())); + path.add(startNodeId); + + final var usedEdges = new HashMap(); + var currentNode = nodes.get(startNodeId); + while (true) { + final var availableEdge = new ArrayList>(); + int totalWeight = 0; + for (final var entry : currentNode.edges.entrySet()) { + final String edgeKey = currentNode.id + " -> " + entry.getKey().id; + if (usedEdges.getOrDefault(edgeKey, 0) < 2) { + availableEdge.add(entry); + totalWeight += entry.getValue(); } + } - final var random = new Random(); - final var nodeIds = new ArrayList<>(nodes.keySet()); - final String startNodeId = nodeIds.get(random.nextInt(nodeIds.size())); - path.add(startNodeId); + if (availableEdge.isEmpty()) { + break; + } - final var usedEdges = new HashMap(); - var currentNode = nodes.get(startNodeId); - while (true) { - final var availableEdge = new ArrayList>(); - int totalWeight = 0; - for (final var entry : currentNode.edges.entrySet()) { - final String edgeKey = currentNode.id + " -> " + entry.getKey().id; - if (usedEdges.getOrDefault(edgeKey, 0) < 2) { - availableEdge.add(entry); - totalWeight += entry.getValue(); - } - } + final double randomValue = random.nextDouble() * totalWeight; + double cumulative = 0.0; + Node nextNode = null; - if (availableEdge.isEmpty()) { - break; - } - - final double randomValue = random.nextDouble() * totalWeight; - double cumulative = 0.0; - Node nextNode = null; - - for (final var entry : availableEdge) { - cumulative += entry.getValue(); - if (randomValue < cumulative) { - nextNode = entry.getKey(); - break; - } - } - - assert nextNode != null; - final String edgeKey = currentNode.id + " -> " + nextNode.id; - final int edgeTimes = usedEdges.getOrDefault(edgeKey, 0) + 1; - usedEdges.put(edgeKey, edgeTimes); - path.add(nextNode.id); - currentNode = nextNode; - - if (edgeTimes >= 2) { - break; - } + for (final var entry : availableEdge) { + cumulative += entry.getValue(); + if (randomValue < cumulative) { + nextNode = entry.getKey(); + break; } - return path; + } + + assert nextNode != null; + final String edgeKey = currentNode.id + " -> " + nextNode.id; + final int edgeTimes = usedEdges.getOrDefault(edgeKey, 0) + 1; + usedEdges.put(edgeKey, edgeTimes); + path.add(nextNode.id); + currentNode = nextNode; + + if (edgeTimes >= 2) { + break; + } + } + return path; + } + + /** + * Generate DOT lang to describe the graph. + * + * @param callback function to modify the option of the node + * @return string of DOT lang + */ + @SuppressWarnings("checkstyle:AbbreviationAsWordInName") + public String toDOT(final Function callback) { + final var sb = new StringBuilder(); + sb.append("digraph G {\n"); + for (final Node node : nodes.values()) { + for (final String edge : node.getDOTEdges(callback)) { + sb.append(" ").append(edge); + } + } + sb.append("}"); + return sb.toString(); + } + + /** + * Generate DOT lang to describe the graph without callback lambda function. + * + * @return string of DOT lang + */ + @SuppressWarnings("checkstyle:AbbreviationAsWordInName") + public String toDOT() { + final var sb = new StringBuilder(); + sb.append("digraph G {\n"); + for (final Node node : nodes.values()) { + for (final String edge : node.getDOTEdges()) { + sb.append(" ").append(edge); + } + } + sb.append("}"); + return sb.toString(); + } + + private void parse(final String input) { + final String[] tokens = input.toLowerCase(Locale.ENGLISH).split("[ \n\r]+"); + final var words = new ArrayList(); + + for (final var token : tokens) { + final String word = token.replaceAll("[^a-zA-Z]", ""); + if (!word.isEmpty()) { + words.add(word); + } } - public String toDOT(final Function callback) { - final var sb = new StringBuilder(); - sb.append("digraph G {\n"); - for (final Node node : nodes.values()) { - for (final String edge : node.getDOTEdges(callback)) { - sb.append(" ").append(edge); - } - } - sb.append("}"); - return sb.toString(); + for (int i = 0; i < words.size() - 1; i++) { + final String currentWord = words.get(i); + final String nextWord = words.get(i + 1); + + final Node currentNode = getOrCreateNode(currentWord); + final Node nextNode = getOrCreateNode(nextWord); + + currentNode.addEdge(nextNode); + } + } + + private Node getOrCreateNode(final String word) { + return nodes.computeIfAbsent(word, Node::new); + } + + private void initRageRanks(final double d) { + final int totalNodes = nodes.size(); + if (totalNodes == 0) { + pageRanks = new HashMap<>(); + return; + } + var myPageRanks = new HashMap(); + final double initRank = 1.0 / totalNodes; + for (final var n : nodes.values()) { + myPageRanks.put(n, initRank); } - public String toDOT() { - final var sb = new StringBuilder(); - sb.append("digraph G {\n"); - for (final Node node : nodes.values()) { - for (final String edge : node.getDOTEdges()) { - sb.append(" ").append(edge).append("\n"); - } + final int iterations = 100; + for (int i = 0; i < iterations; i++) { + final var newPageRanks = new HashMap(); + nodes.values().forEach(n -> newPageRanks.put(n, 0.0)); + for (final var curr : nodes.values()) { + final int totalEdges = curr.edges.size(); + + for (final var entry : curr.edges.entrySet()) { + final Node neighbor = entry.getKey(); + final double contribution = myPageRanks.get(curr) / totalEdges; + newPageRanks.put(neighbor, newPageRanks.get(neighbor) + contribution); } - sb.append("}"); - return sb.toString(); + } + + final double damplingTerm = (1.0 - d) / totalNodes; + nodes.values().forEach(n -> newPageRanks.put(n, damplingTerm + d * newPageRanks.get(n))); + myPageRanks = newPageRanks; } - private void parse(final String input) { - final String[] tokens = input.toLowerCase(Locale.ENGLISH).split("[ \n\r]+"); - final var words = new ArrayList(); + pageRanks = myPageRanks; + for (final var node : nodes.values()) { + if (node.edges.isEmpty()) { + nodes.values().stream() + .filter(n -> !n.equals(node)) + .forEach(n -> pageRanks + .put(n, pageRanks.get(n) + pageRanks.get(node) / (totalNodes - 1))); + pageRanks.put(node, 0.0); + } + } + } - for (final var token : tokens) { - final String word = token.replaceAll("[^a-zA-Z]", ""); - if (!word.isEmpty()) { - words.add(word); - } - } + /** + * Record of edge. + * + * @param node1 fist node + * @param node2 second node + * @param weight weight of edge + * + */ + public record EdgeRecord(String node1, String node2, int weight) { + } - for (int i = 0; i < words.size() - 1; i++) { - final String currentWord = words.get(i); - final String nextWord = words.get(i + 1); + private static class Node { - final Node currentNode = getOrCreateNode(currentWord); - final Node nextNode = getOrCreateNode(nextWord); + String id; + Map edges; - currentNode.addEdge(nextNode); - } + Node(final String id) { + this.id = id; + this.edges = new HashMap<>(); } - private Node getOrCreateNode(final String word) { - return nodes.computeIfAbsent(word, Node::new); + void addEdge(final Node target) { + edges.put(target, edges.getOrDefault(target, 0) + 1); } - private void initRageRanks(final double d) { - final int totalNodes = nodes.size(); - if (totalNodes == 0) { - pageRanks = new HashMap<>(); - return; - } - var myPageRanks = new HashMap(); - final double initRank = 1.0 / totalNodes; - for (final var n : nodes.values()) { - myPageRanks.put(n, initRank); - } - - final int iterations = 100; - for (int i = 0; i < iterations; i++) { - final var newPageRanks = new HashMap(); - nodes.values().forEach(n -> newPageRanks.put(n, 0.0)); - for (final var curr : nodes.values()) { - final int totalEdges = curr.edges.size(); - - for (final var entry : curr.edges.entrySet()) { - final Node neighbor = entry.getKey(); - final double contribution = myPageRanks.get(curr) / totalEdges; - newPageRanks.put(neighbor, newPageRanks.get(neighbor) + contribution); - } - } - - final double damplingTerm = (1.0 - d) / totalNodes; - nodes.values().forEach(n -> newPageRanks.put(n, damplingTerm + d * newPageRanks.get(n))); - myPageRanks = newPageRanks; - } - - pageRanks = myPageRanks; - for (final var node : nodes.values()) { - if (node.edges.isEmpty()) { - nodes.values().stream().filter(n -> !n.equals(node)) - .forEach(n -> pageRanks.put(n, pageRanks.get(n) + pageRanks.get(node) / (totalNodes - 1))); - pageRanks.put(node, 0.0); - } - } + @SuppressWarnings("checkstyle:AbbreviationAsWordInName") + List getDOTEdges(final Function callback) { + final var dotEdges = new ArrayList(); + for (final var entry : edges.entrySet()) { + final Node target = entry.getKey(); + final int weight = entry.getValue(); + dotEdges.add( + callback.apply(new EdgeRecord(id, target.id, weight)) + ";\n"); + } + return dotEdges; } + + @SuppressWarnings("checkstyle:AbbreviationAsWordInName") + List getDOTEdges() { + final var dotEdges = new ArrayList(); + for (final var entry : edges.entrySet()) { + final Node target = entry.getKey(); + final int weight = entry.getValue(); + dotEdges.add( + String.format("%s -> %s [label=\"%d\"];%n", id, target.id, weight)); + } + return dotEdges; + } + } } diff --git a/app/src/main/java/fun/youthlic/GraphCLI.java b/app/src/main/java/fun/youthlic/GraphCLI.java index cc5b3c8..73a16c5 100644 --- a/app/src/main/java/fun/youthlic/GraphCLI.java +++ b/app/src/main/java/fun/youthlic/GraphCLI.java @@ -1,28 +1,343 @@ package fun.youthlic; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; import java.io.IOException; - +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.jline.reader.LineReader; import org.jline.reader.LineReaderBuilder; import org.jline.reader.impl.DefaultParser; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; +/** + * Class for running in Commandline. + */ +@SuppressWarnings("ALL") public class GraphCLI { - - public static void main(final String[] args) { - try { - final Terminal terminal = TerminalBuilder.builder().system(true).build(); - final LineReader reader = LineReaderBuilder.builder().terminal(terminal).parser(new DefaultParser()) - .build(); - while (true) { - final String line = reader.readLine("graph> "); - if (line.trim().equalsIgnoreCase("exit")) - break; - System.out.println(line); - } - } catch (final IOException e) { - } finally { - } + static String readFile(final String filename) { + String text = null; + try (BufferedReader inputReader = new BufferedReader(new FileReader(filename))) { + text = inputReader.lines().collect(Collectors.joining(System.lineSeparator())); + } catch (final IOException e) { + System.out.println("Error reading file: " + e.getMessage()); } + return text; + } + + /** + * CommandLine entry point. + * + * @param args launch args + */ + public static void main(final String[] args) { + GraphCLIHelper helper = null; + Terminal terminal = null; + try { + terminal = TerminalBuilder.builder().system(true).build(); + } catch (final IOException e) { + e.printStackTrace(); + System.exit(-1); + } + final LineReader reader = LineReaderBuilder.builder().terminal(terminal) + .parser(new DefaultParser()) + .build(); + while (true) { + final String line = reader.readLine("graph> "); + final var commands = line.split("[ \\t]+"); + if (commands.length == 0) { + continue; + } + switch (commands[0]) { + case "load": + if (commands.length != 2) { + System.out.println("Usage: load "); + } else { + final var text = readFile(commands[1]); + if (text == null) { + System.out.println("Load file failed; " + commands[1]); + continue; + } + helper = new GraphCLIHelper(text); + } + break; + case "help": + System.out.println("Available commands:"); + System.out.println(" show"); + System.out.println(" dot"); + System.out.println(" help"); + System.out.println(" exit"); + System.out.println(" random-walk"); + System.out.println(" load "); + System.out.println(" page-rank "); + System.out.println(" bridge-words "); + System.out.println(" new-text "); + System.out.println(" shortest-path "); + break; + case "shortest-path": + if (commands.length != 3) { + System.out.println("Usage: shortest-path "); + } else if (helper == null) { + System.out.println("No graph loaded"); + break; + } else { + try { + System.out.println(helper.calcShortestPath(commands[1], commands[2])); + } catch (IOException | InterruptedException e) { + System.out.println(e.getMessage()); + } + } + break; + case "exit": + return; + case "random-walk": + if (commands.length != 1) { + System.out.println("Usage: random-walk"); + } else { + if (helper == null) { + System.out.println("No graph loaded"); + break; + } + System.out.println(helper.randomWalk()); + } + break; + case "new-text": + if (commands.length < 2) { + System.out.println("Usage: new-text "); + } else if (helper == null) { + System.out.println("No graph loaded"); + break; + } else { + System.out.println(helper.generateNewText( + String.join(" ", Arrays.asList(commands).subList(1, commands.length)))); + } + break; + case "show": + if (commands.length != 1) { + System.out.println("Usage: show"); + } else { + if (helper == null) { + System.out.println("No graph loaded"); + break; + } + GraphCLIHelper.showDirectedGraph(helper.graph); + } + break; + case "dot": + if (commands.length != 1) { + System.out.println("Usage: dot"); + } else { + if (helper == null) { + System.out.println("No graph loaded"); + break; + } + System.out.println(helper.graph.toDOT()); + } + break; + case "page-rank": + if (commands.length != 2) { + System.out.println("Usage: page-rank "); + } else if (helper == null) { + System.out.println("No graph loaded"); + } else { + final var pageRank = helper.calPageRank(commands[1]); + if (pageRank < 0.0) { + System.out.println("Invalid word"); + } else { + System.out.println("PageRank of " + commands[1] + ": " + pageRank); + } + } + break; + case "bridge-words": + if (commands.length != 3) { + System.out.println("Usage: bridge-words "); + } else if (helper == null) { + System.out.println("No graph loaded"); + } else { + System.out.println(helper.queryBridgeWords(commands[1], commands[2])); + } + break; + default: + System.out.println("Unknown command: " + commands[0]); + break; + } + } + } +} + +/** + * Helper class for CommandLine. + */ +@SuppressWarnings("ALL") +class GraphCLIHelper { + Graph graph; + + GraphCLIHelper(final String text) { + graph = new Graph(text); + } + + static void showDirectedGraph(final Graph graph) { + final ProcessBuilder processBuilder = new ProcessBuilder("graph-easy", "--as", "ascii"); + processBuilder.redirectErrorStream(true); + Process process; + try { + process = processBuilder.start(); + } catch (final IOException e) { + System.err.println("Failed to start process"); + e.printStackTrace(); + return; + } + try (BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { + writer.write(graph.toDOT()); + writer.flush(); + } catch (final IOException e) { + System.err.println("Failed to write to process"); + e.printStackTrace(); + } + final StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append(System.lineSeparator()); + } + } catch (final IOException e) { + System.err.println("Failed to read from process"); + e.printStackTrace(); + } + try { + final int exitCode = process.waitFor(); + if (exitCode != 0) { + System.err.println("Process exited with code " + exitCode); + } else { + System.out.println(output.toString()); + } + } catch (final InterruptedException e) { + System.err.println("Process interrupted"); + e.printStackTrace(); + } + } + + String queryBridgeWords(final String word1, final String word2) { + final var option = graph.findBridgeWords(word1, word2); + if (option.isEmpty()) { + return "No " + word1 + " or " + word2 + " in the graph!"; + } + final var bridgeWords = option.get(); + if (bridgeWords.isEmpty()) { + return "No bridge words from " + word1 + " to " + word2 + "!"; + } + final var output = new StringBuilder(); + output.append("The bridge words from ").append(word1).append(" to ").append(word2).append(" are: "); + for (int i = 0; i < bridgeWords.size(); i++) { + output.append(bridgeWords.get(i)); + if (i < bridgeWords.size() - 2) { + output.append(", "); + } else if (i == bridgeWords.size() - 2) { + output.append(", and "); + } else { + output.append("."); + } + } + return output.toString(); + } + + String generateNewText(final String inputText) { + final var output = new StringBuilder(); + final var words = inputText.split("\\s+"); + for (int i = 0; i < words.length; i++) { + output.append(words[i]); + if (i < words.length - 1) { + output.append(" "); + final var option = graph.findBridgeWords(words[i], words[i + 1]); + if (option.isPresent() && !option.get().isEmpty()) { + final var bridgeWords = option.get(); + final Random random = new Random(); + final int wordIndex = random.nextInt(bridgeWords.size()); + output.append(option.get().get(wordIndex)).append(" "); + } + } + } + return output.toString(); + } + + String calcShortestPath(final String word1, final String word2) + throws IOException, InterruptedException { + final ProcessBuilder processBuilder = new ProcessBuilder("graph-easy", "--as", "ascii"); + processBuilder.redirectErrorStream(true); + Process process = null; + process = processBuilder.start(); + try (BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { + writer.write(calcShortestPathToDOT(word1, word2)); + writer.flush(); + } + final StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append(System.lineSeparator()); + } + } + try { + final int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Process exited with code " + exitCode); + } else { + return output.toString(); + } + } catch (final InterruptedException e) { + throw e; + } + } + + String calcShortestPathToDOT(final String word1, final String word2) { + var option = graph.findShortestPath(word1, word2); + if (option.isEmpty()) { + return "No " + word1 + " or " + word2 + " in the graph"; + } else { + final var path = option.get(); + if (path.isEmpty()) { + return "No path from " + word1 + " to " + word2; + } + return graph.toDOT(r -> String.format( + "\"%s\" -> \"%s\" [label=\"%d\"%s]", + r.node1() + (path.contains(r.node1()) ? "~" : ""), + r.node2() + (path.contains(r.node2()) ? "~" : ""), + r.weight(), + (path.contains(r.node1()) && path.contains(r.node2()) + && IntStream.range(0, path.size() - 1).anyMatch( + i -> path.get(i).equals(r.node1()) && path.get(i + 1).equals(r.node2()))) + ? ",color=\"red\"" + : "")); + } + } + + Double calPageRank(final String word) { + return graph.computePageRank(word); + } + + String randomWalk() { + final var text = String.join(" ", graph.randomWalk()); + final Path path = Paths.get("output.txt"); + try { + Files.write(path, text.getBytes()); + } catch (final IOException e) { + System.err.println("Failed to write to file"); + e.printStackTrace(); + } + return text; + } } diff --git a/app/src/test/java/fun/youthlic/GraphCLIHelperTest.java b/app/src/test/java/fun/youthlic/GraphCLIHelperTest.java new file mode 100644 index 0000000..7b46438 --- /dev/null +++ b/app/src/test/java/fun/youthlic/GraphCLIHelperTest.java @@ -0,0 +1,80 @@ +package fun.youthlic; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GraphCLIHelperTest { + private final GraphCLIHelper graph = new GraphCLIHelper("B D E H A C D H A F D H A B G H A C G H A D G H"); + + @Test + void testWord1NotInGraph() { + var result = graph.queryBridgeWords("x", "a"); + assertEquals("No x or a in the graph!", result); + } + @Test + void testWord2NotInGraph() { + String result = graph.queryBridgeWords("a", "x"); + assertEquals("No a or x in the graph!", result); + } + + @Test + void testBothWordsNotInGraph() { + String result = graph.queryBridgeWords("x", "y"); + assertEquals("No x or y in the graph!", result); + } + + @Test + void testNoBridgeWords() { + String result = graph.queryBridgeWords("a", "b"); + assertEquals("No bridge words from a to b!", result); + } + + @Test + void testSingleBridgeWord() { + String result = graph.queryBridgeWords("b", "e"); + assertEquals("The bridge words from b to e are: d.", result); + } + + @Test + void testTwoBridgeWords() { + String result = graph.queryBridgeWords("c", "h"); + assertEquals("The bridge words from c to h are: g, and d.", result); + } + + @Test + void testThreeBridgeWords() { + String result = graph.queryBridgeWords("a", "g"); + assertEquals("The bridge words from a to g are: c, b, and d.", result); + } + + @Test + void testSameWordInput() { + String result = graph.queryBridgeWords("a", "a"); + assertEquals("No bridge words from a to a!", result); + } + + @Test + void testWord1Empty() { + String result = graph.queryBridgeWords("", "a"); + assertEquals("No or a in the graph!", result); + } + + @Test + void testWord2Empty() { + String result = graph.queryBridgeWords("a", ""); + assertEquals("No a or in the graph!", result); + } + + @Test + void testBothWordsEmpty() { + String result = graph.queryBridgeWords("", ""); + assertEquals("No or in the graph!", result); + } + + @Test + void testCaseMismatch() { + String result = graph.queryBridgeWords("B", "E"); + assertEquals("The bridge words from B to E are: d.", result); + } +} diff --git a/flake.nix b/flake.nix index 42c1f7c..679e187 100644 --- a/flake.nix +++ b/flake.nix @@ -23,6 +23,7 @@ kotlin-language-server ktfmt jdt-language-server + checkstyle graph-easy graphviz