1 module graph;
2 
3 import std.algorithm;
4 import std.range;
5 import std.typecons;
6 version (unittest) import dshould;
7 
8 alias Dependency = Tuple!(string, "client", string, "supplier");
9 
10 void write(Output)(auto ref Output output, const Dependency[] dependencies)
11 {
12     import std.format : formattedWrite;
13 
14     output.put("digraph Dependencies {\n");
15     output.put("node [shape=box];\n");
16     foreach (element; dependencies.elements)
17     {
18         output.formattedWrite!(`"%s"`)(element);
19         output.put('\n');
20     }
21     foreach (dependency; dependencies)
22     {
23         output.formattedWrite!(`"%s" -> "%s"`)(dependency.client, dependency.supplier);
24         output.put('\n');
25     }
26     output.put("}\n");
27 }
28 
29 @("write dependency graph in the DOT language")
30 unittest
31 {
32     import std.array : appender;
33     import std.string : outdent, stripLeft;
34 
35     auto output = appender!string;
36     const dependencies = [Dependency("a", "b")];
37 
38     output.write(dependencies);
39 
40     const expected = `
41         digraph Dependencies {
42         node [shape=box];
43         "a"
44         "b"
45         "a" -> "b"
46         }
47         `;
48 
49     output.data.should.equal(outdent(expected).stripLeft);
50 }
51 
52 void transitiveClosure(ref Dependency[] dependencies)
53 {
54     string[] elements = dependencies.elements;
55 
56     foreach (element; elements)
57         foreach (client; elements)
58             if (dependencies.canFind(Dependency(client, element)))
59                 foreach (supplier; elements)
60                     if (dependencies.canFind(Dependency(element, supplier)))
61                         dependencies.add(Dependency(client, supplier));
62 }
63 
64 Dependency[] transitiveReduction(ref Dependency[] dependencies)
65 {
66     bool[string] mark = null;
67     Dependency[] cyclicDependencies = null;
68 
69     void traverse(string node)
70     {
71         import std.array : array;
72 
73         mark[node] = true;
74         foreach (outEdge; dependencies.filter!(a => a.client == node).array)
75         {
76             if (!dependencies.canFind(outEdge))
77                 continue;
78             if (mark.get(outEdge.supplier, false))
79             {
80                 cyclicDependencies.add(outEdge);
81                 continue;
82             }
83             foreach (inEdge; dependencies.filter!(a => a.supplier == outEdge.supplier).array)
84             {
85                 if (inEdge == outEdge)
86                     continue;
87                 if (mark.get(inEdge.client, false))
88                     dependencies = dependencies.remove!(a => a == inEdge);
89             }
90             traverse(outEdge.supplier);
91         }
92         mark[node] = false;
93     }
94 
95     foreach (element; dependencies.elements)
96         traverse(element);
97 
98     return cyclicDependencies;
99 }
100 
101 @("apply transitive reduction")
102 unittest
103 {
104     auto dependencies = [Dependency("a", "b"), Dependency("b", "c"), Dependency("a", "c")];
105     auto cyclicDependencies = transitiveReduction(dependencies);
106 
107     dependencies.should.equal([Dependency("a", "b"), Dependency("b", "c")]);
108     cyclicDependencies.should.be.empty;
109 }
110 
111 @("apply transitive reduction to cyclic dependencies")
112 unittest
113 {
114     auto dependencies = [Dependency("a", "b"), Dependency("b", "c"), Dependency("c", "a")];
115     auto cyclicDependencies = transitiveReduction(dependencies);
116 
117     dependencies.should.equal([Dependency("a", "b"), Dependency("b", "c"), Dependency("c", "a")]);
118     cyclicDependencies.sort.should.equal(dependencies);
119 }
120 
121 string[] elements(in Dependency[] dependencies)
122 {
123     string[] elements = null;
124 
125     foreach (dependency; dependencies)
126     {
127         elements.add(dependency.client);
128         elements.add(dependency.supplier);
129     }
130     return elements;
131 }
132 
133 void add(Element)(ref Element[] elements, Element element)
134 {
135     if (!elements.canFind(element))
136         elements ~= element;
137 }