Skip to content
This repository was archived by the owner on May 24, 2023. It is now read-only.

Commit f3c3c68

Browse files
committed
Add a topologicalSort function
1 parent 635ff88 commit f3c3c68

File tree

7 files changed

+228
-1
lines changed

7 files changed

+228
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 2.1.0
2+
3+
* Add a `topologicalSort()` function.
4+
15
# 2.0.0
26

37
- **Breaking**: `crawlAsync` will no longer ignore a node from the graph if the

lib/graphs.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
export 'src/crawl_async.dart' show crawlAsync;
6+
export 'src/cycle_exception.dart';
67
export 'src/shortest_path.dart' show shortestPath, shortestPaths;
78
export 'src/strongly_connected_components.dart'
89
show stronglyConnectedComponents;
10+
export 'src/topological_sort.dart';

lib/src/cycle_exception.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
/// An exception indicating that a cycle was detected in a graph that was
6+
/// expected to be acyclic.
7+
class CycleException<T> implements Exception {
8+
/// The list of nodes comprising the cycle.
9+
///
10+
/// Each node in this list has an edge to the next node. The final node has an
11+
/// edge to the first node.
12+
final List<T> cycle;
13+
14+
CycleException(Iterable<T> cycle) : cycle = List.unmodifiable(cycle);
15+
16+
@override
17+
String toString() => 'A cycle was detected in a graph that must be acyclic:\n'
18+
'${cycle.map((node) => '* $node').join('\n')}';
19+
}

lib/src/topological_sort.dart

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:collection';
6+
7+
import 'package:collection/collection.dart';
8+
9+
import 'cycle_exception.dart';
10+
11+
/// Returns a topological sort of [nodes] given the directed edges of a graph
12+
/// provided by [edges].
13+
///
14+
/// Each element of the returned iterable is guaranteed to appear after all
15+
/// nodes that have edges leading to that node. The result is not guaranteed to
16+
/// be unique, nor is it guaranteed to be stable across releases of this
17+
/// package; however, it will be stable for a given input within a given package
18+
/// version.
19+
///
20+
/// If [equals] is provided, it is used to compare nodes in the graph. If
21+
/// [equals] is omitted, the node's own [Object.==] is used instead.
22+
///
23+
/// Similarly, if [hashCode] is provided, it is used to produce a hash value
24+
/// for nodes to efficiently calculate the return value. If it is omitted, the
25+
/// key's own [Object.hashCode] is used.
26+
///
27+
/// If you supply one of [equals] or [hashCode], you should generally also to
28+
/// supply the other.
29+
///
30+
/// Throws a [CycleException<T>] if the graph is cyclical.
31+
List<T> topologicalSort<T>(Iterable<T> nodes, Iterable<T> Function(T) edges,
32+
{bool Function(T, T)? equals, int Function(T)? hashCode}) {
33+
// https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
34+
var result = QueueList<T>();
35+
var permanentMark = HashSet<T>(equals: equals, hashCode: hashCode);
36+
var temporaryMark = LinkedHashSet<T>(equals: equals, hashCode: hashCode);
37+
void visit(T node) {
38+
if (permanentMark.contains(node)) return;
39+
if (temporaryMark.contains(node)) {
40+
throw CycleException(temporaryMark);
41+
}
42+
43+
temporaryMark.add(node);
44+
for (var child in edges(node)) {
45+
visit(child);
46+
}
47+
temporaryMark.remove(node);
48+
permanentMark.add(node);
49+
result.addFirst(node);
50+
}
51+
52+
for (var node in nodes) {
53+
visit(node);
54+
}
55+
return result;
56+
}
57+
58+
bool _defaultEquals(Object a, Object b) => a == b;

pubspec.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
name: graphs
2-
version: 2.0.0
2+
version: 2.1.0
33
description: Graph algorithms that operate on graphs in any representation
44
repository: https://github.com/dart-lang/graphs
55

66
environment:
77
sdk: '>=2.12.0 <3.0.0'
88

9+
dependencies:
10+
collection: ^1.1.0
11+
912
dev_dependencies:
1013
pedantic: ^1.10.0
1114
test: ^1.16.0

test/topological_sort_test.dart

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:collection';
6+
7+
import 'package:graphs/graphs.dart';
8+
import 'package:test/test.dart';
9+
10+
import 'utils/utils.dart';
11+
12+
void main() {
13+
group('sorts a graph', () {
14+
test('with no nodes', () {
15+
expect(_topologicalSort({}), isEmpty);
16+
});
17+
18+
test('with only one node', () {
19+
expect(_topologicalSort({1: []}), equals([1]));
20+
});
21+
22+
test('with no edges', () {
23+
expect(_topologicalSort({1: [], 2: [], 3: [], 4: []}),
24+
unorderedEquals([1, 2, 3, 4]));
25+
});
26+
27+
test('with single edges', () {
28+
expect(
29+
_topologicalSort({
30+
1: [2],
31+
2: [3],
32+
3: [4],
33+
4: []
34+
}),
35+
equals([1, 2, 3, 4]));
36+
});
37+
38+
test('with many edges from one node', () {
39+
var result = _topologicalSort({
40+
1: [2, 3, 4],
41+
2: [],
42+
3: [],
43+
4: []
44+
});
45+
expect(result.indexOf(1), lessThan(result.indexOf(2)));
46+
expect(result.indexOf(1), lessThan(result.indexOf(3)));
47+
expect(result.indexOf(1), lessThan(result.indexOf(4)));
48+
});
49+
50+
test('with transitive edges', () {
51+
var result = _topologicalSort({
52+
1: [2, 4],
53+
2: [],
54+
3: [],
55+
4: [3]
56+
});
57+
expect(result.indexOf(1), lessThan(result.indexOf(2)));
58+
expect(result.indexOf(1), lessThan(result.indexOf(3)));
59+
expect(result.indexOf(1), lessThan(result.indexOf(4)));
60+
expect(result.indexOf(4), lessThan(result.indexOf(3)));
61+
});
62+
63+
test('with diamond edges', () {
64+
var result = _topologicalSort({
65+
1: [2, 3],
66+
2: [4],
67+
3: [4],
68+
4: []
69+
});
70+
expect(result.indexOf(1), lessThan(result.indexOf(2)));
71+
expect(result.indexOf(1), lessThan(result.indexOf(3)));
72+
expect(result.indexOf(1), lessThan(result.indexOf(4)));
73+
expect(result.indexOf(2), lessThan(result.indexOf(4)));
74+
expect(result.indexOf(3), lessThan(result.indexOf(4)));
75+
});
76+
});
77+
78+
test('respects custom equality and hash functions', () {
79+
expect(
80+
_topologicalSort<int>({
81+
0: [2],
82+
3: [4],
83+
5: [6],
84+
7: []
85+
},
86+
equals: (i, j) => (i ~/ 2) == (j ~/ 2),
87+
hashCode: (i) => (i ~/ 2).hashCode),
88+
equals([
89+
0,
90+
anyOf([2, 3]),
91+
anyOf([4, 5]),
92+
anyOf([6, 7])
93+
]));
94+
});
95+
96+
group('throws a CycleException for a graph with', () {
97+
test('a one-node cycle', () {
98+
expect(
99+
() => _topologicalSort({
100+
1: [1]
101+
}),
102+
throwsCycleException([1]));
103+
});
104+
105+
test('a multi-node cycle', () {
106+
expect(
107+
() => _topologicalSort({
108+
1: [2],
109+
2: [3],
110+
3: [4],
111+
4: [1]
112+
}),
113+
throwsCycleException([1, 2, 3, 4]));
114+
});
115+
});
116+
}
117+
118+
/// Runs a topological sort on a graph represented a map from keys to edges.
119+
List<T> _topologicalSort<T>(Map<T, List<T>> graph,
120+
{bool Function(T, T)? equals, int Function(T)? hashCode}) {
121+
if (equals != null) {
122+
graph = LinkedHashMap(equals: equals, hashCode: hashCode)..addAll(graph);
123+
}
124+
return topologicalSort(graph.keys, (node) {
125+
expect(graph, contains(node));
126+
return graph[node]!;
127+
}, equals: equals, hashCode: hashCode);
128+
}

test/utils/utils.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,23 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'package:graphs/graphs.dart';
6+
import 'package:test/test.dart';
7+
58
bool xEquals(X a, X b) => a.value == b.value;
69

710
int xHashCode(X a) => a.value.hashCode;
811

12+
/// Returns a matcher that verifies that a function throws a [CycleException<T>]
13+
/// with the given [cycle].
14+
Matcher throwsCycleException<T>(List<T> cycle) => throwsA(allOf([
15+
isA<CycleException<T>>(),
16+
predicate((exception) {
17+
expect((exception as CycleException<T>).cycle, equals(cycle));
18+
return true;
19+
})
20+
]));
21+
922
class X {
1023
final String value;
1124

0 commit comments

Comments
 (0)