Skip to content

Commit 8d6186e

Browse files
committed
ready for release
1 parent 852cd03 commit 8d6186e

File tree

7 files changed

+66
-55
lines changed

7 files changed

+66
-55
lines changed

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ It includes a `Dag<T>` interface so you can provide your own implementation.
1515
```java
1616
Dag<String> dag = new HashDag<>();
1717

18-
// Add nodes with (parent, child) relationships to the DAG
18+
// Add nodes with source -> target relationships to the DAG
1919
dag.put("Dorothy", "Shelby");
2020
dag.put("Shelby", "Alex");
2121
dag.put("Joe", "Alex");
@@ -24,8 +24,9 @@ dag.put("Joe", "Alex");
2424
dag.add("Clare");
2525
dag.add("Sarah");
2626

27-
// Find a reverse-topologically sorted list of the nodes
27+
// Find a topologically sorted list of the nodes
2828
// Ex: ["Alex", "Joe", "Sarah", "Shelby", "Dorothy", "Clare"]
29+
// Ex: ["Dorothy", "Shelby", "Clare", "Joe", "Alex", "Sarah"]
2930
List<String> sorted = dag.sort();
3031

3132
// Find the root nodes of the DAG
@@ -36,13 +37,13 @@ Set<String> roots = dag.getRoots();
3637
// Ex: ["Alex", "Clare", "Sarah"]
3738
Set<String> leaves = dag.getLeaves();
3839

39-
// Find the parents of a node
40+
// Find a node's incoming nodes
4041
// Ex: ["Joe", "Shelby"]
41-
Set<String> parents = dag.getParents("Alex");
42+
Set<String> incoming = dag.getIncoming("Alex");
4243

43-
// Find the children of a node
44+
// Find a node's outgoing nodes
4445
// Ex: ["Shelby"]
45-
Set<String> children = dag.getChildren("Dorothy");
46+
Set<String> outgoing = dag.getOutgoing("Dorothy");
4647

4748
// Find the ancestors of a node
4849
// Ex: ["Joe", "Shelby", "Dorothy"]
@@ -62,7 +63,7 @@ Dag<String> copy = dag.clone();
6263
### DAG Traversal
6364

6465
You can use a `DagTraversalTask` to run a task on each node in multiple threads. Each node is only visited once all of
65-
its children have been visited. This is useful for running complex multithreaded pipelines on nodes with shared
66+
its incoming nodes have been visited. This is useful for running complex multithreaded pipelines on nodes with shared
6667
dependencies.
6768

6869
```java

src/main/java/me/alexjs/dag/Dag.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public interface Dag<T> extends Collection<T>, Cloneable, Serializable {
4444
boolean removeEdge(T source, T target);
4545

4646
/**
47-
* Orders the nodes of this DAG such that each source node comes before its target nodes in the ordering
47+
* Orders the nodes of this DAG such that each node comes before its outgoing nodes in the ordering
4848
*
4949
* @return a list of nodes in topological order, or {@code null} if there's a circular dependency
5050
* @see <a href="https://en.wikipedia.org/wiki/Topological_sorting">https://en.wikipedia.org/wiki/Topological_sorting</a>

src/main/java/me/alexjs/dag/DagTraversalTask.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ public class DagTraversalTask<T> {
3030
private final Dag<T> dag;
3131
private final Consumer<T> task;
3232
private final ListeningExecutorService executorService;
33-
private final Map<T, Set<T>> parents;
33+
private final Map<T, Set<T>> outgoingNodes;
3434
private final Lock lock;
3535
private final AtomicBoolean failed;
3636

3737
/**
3838
* Create a task that traverses a DAG with an {@link java.util.concurrent.ExecutorService}
3939
* <p>
40-
* The nodes will be traversed in reverse-topological order,
41-
* such that no node is visited until all its children have been visited.
40+
* The nodes will be traversed in topological order,
41+
* such that no node is visited until all its incoming nodes have been visited.
4242
*
4343
* @param dag the DAG to traverse
4444
* @param task the task to apply to each node
@@ -49,21 +49,21 @@ public DagTraversalTask(Dag<T> dag, Consumer<T> task, ExecutorService executorSe
4949
this.dag = dag.clone();
5050
this.task = task;
5151
this.executorService = MoreExecutors.listeningDecorator(executorService);
52-
this.parents = new HashMap<>();
52+
this.outgoingNodes = new HashMap<>();
5353
this.lock = new ReentrantLock(true);
5454
this.failed = new AtomicBoolean();
5555

56-
// Cache the parents of each node for this DAG
57-
this.dag.getNodes().forEach(node -> this.parents.put(node, this.dag.getIncoming(node)));
56+
// Cache each node's outgoing nodes for this DAG
57+
this.dag.getNodes().forEach(node -> this.outgoingNodes.put(node, this.dag.getOutgoing(node)));
5858

5959
// Get the set of leaves for this dag
60-
Set<T> leaves = this.dag.getLeaves();
60+
Set<T> roots = this.dag.getRoots();
6161

6262
// If there are no leaves, then there are no nodes to visit
63-
if (leaves.isEmpty()) {
63+
if (roots.isEmpty()) {
6464
executorService.shutdown();
6565
} else {
66-
visit(leaves);
66+
visit(roots);
6767
}
6868

6969
}
@@ -101,13 +101,13 @@ private void visit(Collection<T> nodes) {
101101
executorService.shutdown();
102102
}
103103

104-
Set<T> parents = this.parents.get(node);
105-
parents.retainAll(dag.getNodes());
106-
parents.removeIf(p -> !dag.getOutgoing(p).isEmpty());
104+
Set<T> outgoing = this.outgoingNodes.get(node);
105+
outgoing.retainAll(dag.getNodes());
106+
outgoing.removeIf(p -> !dag.getIncoming(p).isEmpty());
107107

108108
lock.unlock();
109109

110-
visit(parents);
110+
visit(outgoing);
111111

112112
}, MoreExecutors.directExecutor());
113113
} catch (Throwable ignore) {

src/main/java/me/alexjs/dag/HashDag.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,21 @@ public List<E> sort() {
8181
List<E> sorted = new LinkedList<>();
8282
Deque<E> s = new LinkedList<>(getRoots());
8383

84-
Dag<E> copy = clone();
84+
Map<E, Collection<E>> copy = this.toMap();
8585

8686
while (!s.isEmpty()) {
8787
E n = s.pop();
8888
sorted.add(n);
89-
for (E m : copy.getOutgoing(n)) {
90-
copy.removeEdge(n, m);
91-
if (copy.getIncoming(m).isEmpty()) {
89+
90+
for (E m : copy.remove(n)) {
91+
boolean hasIncoming = false;
92+
for (Collection<E> entry : copy.values()) {
93+
if (entry.contains(m)) {
94+
hasIncoming = true;
95+
break;
96+
}
97+
}
98+
if (!hasIncoming) {
9299
s.add(m);
93100
}
94101
}

src/test/java/me/alexjs/dag/TestDag.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public static void init() {
1818
@RepeatedTest(50)
1919
public void testSort() {
2020

21-
Dag<Integer> dag = helper.populateDag();
21+
Dag<Integer> dag = helper.populateDagSimple();
2222

2323
List<Integer> sorted = dag.sort();
2424

@@ -94,23 +94,23 @@ public void testEmptyDag() {
9494
Set<Integer> leaves = dag.getLeaves();
9595
Set<Integer> ancestors = dag.getAncestors(0);
9696
Set<Integer> descendants = dag.getDescendants(0);
97-
Set<Integer> parents = dag.getIncoming(0);
98-
Set<Integer> children = dag.getOutgoing(0);
97+
Set<Integer> incoming = dag.getIncoming(0);
98+
Set<Integer> outgoing = dag.getOutgoing(0);
9999

100100
Assertions.assertTrue(roots.isEmpty());
101101
Assertions.assertTrue(leaves.isEmpty());
102102
Assertions.assertTrue(ancestors.isEmpty());
103103
Assertions.assertTrue(descendants.isEmpty());
104-
Assertions.assertTrue(parents.isEmpty());
105-
Assertions.assertTrue(children.isEmpty());
104+
Assertions.assertTrue(incoming.isEmpty());
105+
Assertions.assertTrue(outgoing.isEmpty());
106106

107107
Iterator<Integer> it = dag.iterator();
108108
Assertions.assertFalse(it.hasNext());
109109

110110
}
111111

112112
@Test
113-
public void testNoChildren() {
113+
public void testNoOutgoing() {
114114

115115
// Test with putAll()
116116
Dag<Integer> dag = new HashDag<>();

src/test/java/me/alexjs/dag/TestDagCollection.java

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,12 @@ public void testRemoveAndRetain() {
3838
Assertions.assertTrue(dag.removeAll(List.of(5, 6, 7)));
3939
Assertions.assertTrue(dag.isEmpty());
4040

41-
dag.put(10, 11);
42-
dag.put(10, 12);
43-
dag.put(13, 14);
44-
dag.put(15, 14);
45-
Assertions.assertEquals(2, dag.getOutgoing(10).size());
46-
Assertions.assertEquals(2, dag.getIncoming(14).size());
47-
48-
Assertions.assertTrue(dag.retainAll(List.of(10, 14)));
49-
Assertions.assertEquals(0, dag.getOutgoing(10).size());
50-
Assertions.assertEquals(0, dag.getIncoming(14).size());
41+
dag.put(8, 9);
42+
Assertions.assertTrue(dag.removeEdge(8, 9));
43+
Assertions.assertTrue(dag.contains(8));
44+
Assertions.assertTrue(dag.contains(9));
45+
Assertions.assertTrue(dag.getOutgoing(8).isEmpty());
46+
Assertions.assertTrue(dag.getIncoming(9).isEmpty());
5147

5248
}
5349

@@ -58,12 +54,22 @@ public void testSize() {
5854

5955
Assertions.assertTrue(dag.size() > 0);
6056
Assertions.assertFalse(dag.isEmpty());
61-
6257
dag.clear();
6358

6459
Assertions.assertEquals(0, dag.size());
6560
Assertions.assertTrue(dag.isEmpty());
6661

62+
dag.put(1, 2);
63+
dag.put(1, 3);
64+
dag.put(4, 6);
65+
dag.put(5, 6);
66+
Assertions.assertEquals(2, dag.getOutgoing(1).size());
67+
Assertions.assertEquals(2, dag.getIncoming(6).size());
68+
69+
Assertions.assertTrue(dag.retainAll(List.of(1, 6)));
70+
Assertions.assertEquals(0, dag.getOutgoing(1).size());
71+
Assertions.assertEquals(0, dag.getIncoming(6).size());
72+
6773
}
6874

6975
@Test

src/test/java/me/alexjs/dag/TestingHelper.java

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,10 @@ public TestingHelper() {
2525

2626
public void assertOrder(Dag<Integer> dag, List<Integer> sorted) {
2727
Assertions.assertEquals(dag.getNodes().size(), sorted.size());
28-
for (Integer parent : sorted) {
29-
// If a parent comes before any of its children, then fail
30-
for (Integer child : dag.getOutgoing(parent)) {
31-
if (sorted.indexOf(parent) < sorted.indexOf(child)) {
32-
Assertions.fail();
33-
return;
34-
}
28+
for (Integer node : sorted) {
29+
// If a node comes before any of its outgoing nodes, then fail
30+
for (Integer outgoing : dag.getOutgoing(node)) {
31+
Assertions.assertTrue(sorted.indexOf(node) < sorted.indexOf(outgoing));
3532
}
3633
}
3734
}
@@ -47,17 +44,17 @@ public int getMiddleNode(Dag<Integer> dag) {
4744

4845
public Dag<Integer> populateDag() {
4946

50-
// Add a ton of parent-child relationships. Many nodes will have multiple children
47+
// Add a ton of source-target relationships. Many nodes will have multiple outgoing edges
5148
Dag<Integer> dag = new HashDag<>();
5249
int nodes = random.nextInt(5000) + 5000;
5350
for (int i = 0; i < nodes; i++) {
54-
// A parent will always be strictly less than its children to ensure no circular dependencies
55-
int parent = random.nextInt(500);
56-
int child = parent + random.nextInt(500) + 1;
57-
dag.put(parent, child);
51+
// Each node will always be strictly less than its outgoing nodes to ensure no circular dependencies
52+
int source = random.nextInt(500);
53+
int target = source + random.nextInt(500) + 1;
54+
dag.put(source, target);
5855
}
5956

60-
// Nodes that are guaranteed to have no parents or children
57+
// Nodes that are guaranteed to have no incoming or outgoing edges
6158
ArrayList<Integer> orphans = IntStream.generate(() -> random.nextInt(2000) + 1000)
6259
.limit(100)
6360
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

0 commit comments

Comments
 (0)