├── .gitignore ├── .github ├── dependabot.yaml └── workflows │ └── ci.yml ├── pubspec.yaml ├── lib ├── graphs.dart └── src │ ├── cycle_exception.dart │ ├── crawl_async.dart │ ├── strongly_connected_components.dart │ ├── shortest_path.dart │ ├── transitive_closure.dart │ └── topological_sort.dart ├── test ├── utils │ ├── utils.dart │ └── graph.dart ├── crawl_async_test.dart ├── shortest_path_test.dart ├── strongly_connected_components_test.dart ├── transitive_closure_test.dart └── topological_sort_test.dart ├── example ├── example.dart └── crawl_async_example.dart ├── benchmark ├── connected_components_benchmark.dart ├── shortest_path_benchmark.dart └── shortest_path_worst_case_benchmark.dart ├── LICENSE ├── README.md ├── CONTRIBUTING.md ├── analysis_options.yaml └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | .pub/ 4 | build/ 5 | pubspec.lock 6 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: monthly 9 | labels: 10 | - autosubmit 11 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: graphs 2 | version: 2.3.0 3 | description: Graph algorithms that operate on graphs in any representation 4 | repository: https://github.com/dart-lang/graphs 5 | 6 | environment: 7 | sdk: '>=2.18.0 <3.0.0' 8 | 9 | dependencies: 10 | collection: ^1.1.0 11 | 12 | dev_dependencies: 13 | lints: ^2.0.0 14 | test: ^1.16.0 15 | 16 | # For examples 17 | analyzer: ^5.2.0 18 | path: ^1.8.0 19 | pool: ^1.5.0 20 | -------------------------------------------------------------------------------- /lib/graphs.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, 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 | export 'src/crawl_async.dart' show crawlAsync; 6 | export 'src/cycle_exception.dart' show CycleException; 7 | export 'src/shortest_path.dart' show shortestPath, shortestPaths; 8 | export 'src/strongly_connected_components.dart' 9 | show stronglyConnectedComponents; 10 | export 'src/topological_sort.dart' show topologicalSort; 11 | export 'src/transitive_closure.dart' show transitiveClosure; 12 | -------------------------------------------------------------------------------- /lib/src/cycle_exception.dart: -------------------------------------------------------------------------------- 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 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 cycle; 13 | 14 | CycleException(Iterable 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 | } 20 | -------------------------------------------------------------------------------- /test/utils/utils.dart: -------------------------------------------------------------------------------- 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 'package:graphs/graphs.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | bool xEquals(X a, X b) => a.value == b.value; 9 | 10 | int xHashCode(X a) => a.value.hashCode; 11 | 12 | /// Returns a matcher that verifies that a function throws a [CycleException] 13 | /// with the given [cycle]. 14 | Matcher throwsCycleException(List cycle) => throwsA( 15 | allOf([ 16 | isA>(), 17 | predicate((exception) { 18 | expect((exception as CycleException).cycle, equals(cycle)); 19 | return true; 20 | }) 21 | ]), 22 | ); 23 | 24 | class X { 25 | final String value; 26 | 27 | X(this.value); 28 | 29 | @override 30 | bool operator ==(Object other) => throw UnimplementedError(); 31 | 32 | @override 33 | int get hashCode => 42; 34 | 35 | @override 36 | String toString() => '($value)'; 37 | } 38 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, 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 'package:graphs/graphs.dart'; 6 | 7 | /// A representation of a directed graph. 8 | /// 9 | /// Data is stored on the [Node] class. 10 | class Graph { 11 | final Map> nodes; 12 | 13 | Graph(this.nodes); 14 | } 15 | 16 | class Node { 17 | final String id; 18 | final int data; 19 | 20 | Node(this.id, this.data); 21 | 22 | @override 23 | bool operator ==(Object other) => other is Node && other.id == id; 24 | 25 | @override 26 | int get hashCode => id.hashCode; 27 | 28 | @override 29 | String toString() => '<$id -> $data>'; 30 | } 31 | 32 | void main() { 33 | final nodeA = Node('A', 1); 34 | final nodeB = Node('B', 2); 35 | final nodeC = Node('C', 3); 36 | final nodeD = Node('D', 4); 37 | final graph = Graph({ 38 | nodeA: [nodeB, nodeC], 39 | nodeB: [nodeC, nodeD], 40 | nodeC: [nodeB, nodeD] 41 | }); 42 | 43 | final components = stronglyConnectedComponents( 44 | graph.nodes.keys, 45 | (node) => graph.nodes[node] ?? [], 46 | ); 47 | 48 | print(components); 49 | } 50 | -------------------------------------------------------------------------------- /test/utils/graph.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, 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 'utils.dart'; 8 | 9 | /// A representation of a Graph since none is specified in `lib/`. 10 | class Graph { 11 | final Map?> _graph; 12 | 13 | Graph(this._graph); 14 | 15 | List edges(String node) => _graph[node] ?? []; 16 | 17 | Iterable get allNodes => _graph.keys; 18 | } 19 | 20 | class BadGraph { 21 | final Map?> _graph; 22 | 23 | BadGraph(Map?> values) 24 | : _graph = LinkedHashMap(equals: xEquals, hashCode: xHashCode) 25 | ..addEntries( 26 | values.entries 27 | .map((e) => MapEntry(X(e.key), e.value?.map(X.new).toList())), 28 | ); 29 | 30 | List edges(X node) => _graph[node] ?? []; 31 | 32 | Iterable get allNodes => _graph.keys; 33 | } 34 | 35 | /// A representation of a Graph where keys can asynchronously be resolved to 36 | /// real values or to edges. 37 | class AsyncGraph { 38 | final Map?> graph; 39 | 40 | AsyncGraph(this.graph); 41 | 42 | Future readNode(String node) async => 43 | graph.containsKey(node) ? node : null; 44 | 45 | Future> edges(String key, String? node) async => 46 | graph[key] ?? []; 47 | } 48 | -------------------------------------------------------------------------------- /benchmark/connected_components_benchmark.dart: -------------------------------------------------------------------------------- 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 | import 'dart:math' show Random; 7 | 8 | import 'package:graphs/graphs.dart'; 9 | 10 | void main() { 11 | final rnd = Random(0); 12 | const size = 2000; 13 | final graph = HashMap>(); 14 | 15 | for (var i = 0; i < size * 3; i++) { 16 | final toList = graph.putIfAbsent(rnd.nextInt(size), () => []); 17 | 18 | final toValue = rnd.nextInt(size); 19 | if (!toList.contains(toValue)) { 20 | toList.add(toValue); 21 | } 22 | } 23 | 24 | var maxCount = 0; 25 | var maxIteration = 0; 26 | 27 | const duration = Duration(milliseconds: 100); 28 | 29 | for (var i = 1;; i++) { 30 | var count = 0; 31 | final watch = Stopwatch()..start(); 32 | while (watch.elapsed < duration) { 33 | count++; 34 | final length = 35 | stronglyConnectedComponents(graph.keys, (e) => graph[e] ?? []) 36 | .length; 37 | assert(length == 244, '$length'); 38 | } 39 | 40 | if (count > maxCount) { 41 | maxCount = count; 42 | maxIteration = i; 43 | } 44 | 45 | if (maxIteration == i || (i - maxIteration) % 20 == 0) { 46 | print('max iterations in ${duration.inMilliseconds}ms: $maxCount\t' 47 | 'after $maxIteration of $i iterations'); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This package has moved! 2 | 3 | __The package can now be found at https://github.com/dart-lang/tools/tree/main/pkgs/graphs__ 4 | 5 | 6 | [![CI](https://github.com/dart-lang/graphs/actions/workflows/ci.yml/badge.svg)](https://github.com/dart-lang/graphs/actions/workflows/ci.yml) 7 | [![pub package](https://img.shields.io/pub/v/graphs.svg)](https://pub.dev/packages/graphs) 8 | [![package publisher](https://img.shields.io/pub/publisher/graphs.svg)](https://pub.dev/packages/graphs/publisher) 9 | 10 | Graph algorithms that do not specify a particular approach for representing a 11 | Graph. 12 | 13 | Functions in this package will take arguments that provide the mechanism for 14 | traversing the graph. For example two common approaches for representing a 15 | graph: 16 | 17 | ```dart 18 | class Graph { 19 | Map> nodes; 20 | } 21 | class Node { 22 | // Interesting data 23 | } 24 | ``` 25 | 26 | ```dart 27 | class Graph { 28 | Node root; 29 | } 30 | class Node { 31 | List edges; 32 | // Interesting data 33 | } 34 | ``` 35 | 36 | Any representation can be adapted to the needs of the algorithm: 37 | 38 | - Some algorithms need to associate data with each node in the graph. If the 39 | node type `T` does not correctly or efficiently implement `hashCode` or `==`, 40 | you may provide optional `equals` and/or `hashCode` functions are parameters. 41 | - Algorithms which need to traverse the graph take a `edges` function which provides the reachable nodes. 42 | - `(node) => graph[node]` 43 | - `(node) => node.edges` 44 | 45 | 46 | Graphs that are resolved asynchronously will have similar functions which 47 | return `FutureOr`. 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Check code formatting and static analysis on a single OS (linux) 17 | # against Dart beta. 18 | analyze: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | sdk: [dev] 24 | steps: 25 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab 26 | - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f 27 | with: 28 | sdk: ${{ matrix.sdk }} 29 | - id: install 30 | run: dart pub get 31 | - run: dart format --output=none --set-exit-if-changed . 32 | if: always() && steps.install.outcome == 'success' 33 | - run: dart analyze --fatal-infos 34 | if: always() && steps.install.outcome == 'success' 35 | 36 | test: 37 | needs: analyze 38 | runs-on: ${{ matrix.os }} 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | os: [ubuntu-latest] 43 | sdk: [2.18.0, dev] 44 | steps: 45 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab 46 | - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f 47 | with: 48 | sdk: ${{ matrix.sdk }} 49 | - id: install 50 | run: dart pub get 51 | - run: dart test --platform vm 52 | if: always() && steps.install.outcome == 'success' 53 | - run: dart test --platform chrome 54 | if: always() && steps.install.outcome == 'success' 55 | -------------------------------------------------------------------------------- /benchmark/shortest_path_benchmark.dart: -------------------------------------------------------------------------------- 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 | import 'dart:math' show Random; 7 | 8 | import 'package:graphs/graphs.dart'; 9 | 10 | void main() { 11 | final rnd = Random(1); 12 | const size = 1000; 13 | final graph = HashMap>(); 14 | 15 | for (var i = 0; i < size * 5; i++) { 16 | final toList = graph.putIfAbsent(rnd.nextInt(size), () => []); 17 | 18 | final toValue = rnd.nextInt(size); 19 | if (!toList.contains(toValue)) { 20 | toList.add(toValue); 21 | } 22 | } 23 | 24 | int? minTicks; 25 | var maxIteration = 0; 26 | 27 | final testOutput = 28 | shortestPath(0, size - 1, (e) => graph[e] ?? []).toString(); 29 | print(testOutput); 30 | assert(testOutput == '(258, 252, 819, 999)', testOutput); 31 | 32 | final watch = Stopwatch(); 33 | for (var i = 1;; i++) { 34 | watch 35 | ..reset() 36 | ..start(); 37 | final result = shortestPath(0, size - 1, (e) => graph[e] ?? [])!; 38 | final length = result.length; 39 | final first = result.first; 40 | watch.stop(); 41 | assert(length == 4, '$length'); 42 | assert(first == 258, '$first'); 43 | 44 | if (minTicks == null || watch.elapsedTicks < minTicks) { 45 | minTicks = watch.elapsedTicks; 46 | maxIteration = i; 47 | } 48 | 49 | if (maxIteration == i || (i - maxIteration) % 100000 == 0) { 50 | print('min ticks for one run: $minTicks\t' 51 | 'after $maxIteration of $i iterations'); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | Before we can use your code, you must sign the 6 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. 22 | 23 | ### File headers 24 | All files in the project must start with the following header. 25 | 26 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 27 | // for details. All rights reserved. Use of this source code is governed by a 28 | // BSD-style license that can be found in the LICENSE file. 29 | 30 | ### The small print 31 | Contributions made by corporations are covered by a different agreement than the 32 | one above, the 33 | [Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). 34 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/language/analysis-options 2 | include: package:lints/recommended.yaml 3 | 4 | analyzer: 5 | language: 6 | strict-casts: true 7 | strict-inference: true 8 | strict-raw-types: true 9 | 10 | linter: 11 | rules: 12 | - always_declare_return_types 13 | - avoid_bool_literals_in_conditional_expressions 14 | - avoid_catching_errors 15 | - avoid_classes_with_only_static_members 16 | - avoid_dynamic_calls 17 | - avoid_private_typedef_functions 18 | - avoid_redundant_argument_values 19 | - avoid_returning_null 20 | - avoid_returning_null_for_future 21 | - avoid_returning_this 22 | - avoid_unused_constructor_parameters 23 | - avoid_void_async 24 | - cancel_subscriptions 25 | - comment_references 26 | - directives_ordering 27 | - join_return_with_assignment 28 | - lines_longer_than_80_chars 29 | - literal_only_boolean_expressions 30 | - missing_whitespace_between_adjacent_strings 31 | - no_adjacent_strings_in_list 32 | - no_runtimeType_toString 33 | - omit_local_variable_types 34 | - only_throw_errors 35 | - package_api_docs 36 | - prefer_asserts_in_initializer_lists 37 | - prefer_const_constructors 38 | - prefer_const_declarations 39 | - prefer_expression_function_bodies 40 | - prefer_final_locals 41 | - prefer_relative_imports 42 | - prefer_single_quotes 43 | - require_trailing_commas 44 | - test_types_in_equals 45 | - throw_in_finally 46 | - type_annotate_public_apis 47 | - unawaited_futures 48 | - unnecessary_await_in_return 49 | - unnecessary_lambdas 50 | - unnecessary_parenthesis 51 | - unnecessary_raw_strings 52 | - unnecessary_statements 53 | - use_if_null_to_convert_nulls_to_bools 54 | - use_raw_strings 55 | - use_string_buffers 56 | - use_super_parameters 57 | -------------------------------------------------------------------------------- /benchmark/shortest_path_worst_case_benchmark.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, 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 | 9 | void main() { 10 | const size = 1000; 11 | final graph = HashMap>(); 12 | 13 | // We create a graph where every subsequent node has an edge to every other 14 | // node before it as well as the next node. This triggers worst case behavior 15 | // in many algorithms as it requires visiting all nodes and edges before 16 | // finding a solution, and there are a maximum number of edges. 17 | for (var i = 0; i < size; i++) { 18 | final toList = graph.putIfAbsent(i, () => []); 19 | for (var t = 0; t < i + 2 && i < size; t++) { 20 | if (i == t) continue; 21 | toList.add(t); 22 | } 23 | } 24 | 25 | int? minTicks; 26 | var maxIteration = 0; 27 | 28 | final testOutput = 29 | shortestPath(0, size - 1, (e) => graph[e] ?? []).toString(); 30 | print(testOutput); 31 | assert( 32 | testOutput == Iterable.generate(size - 1, (i) => i + 1).toString(), 33 | testOutput, 34 | ); 35 | 36 | final watch = Stopwatch(); 37 | for (var i = 1;; i++) { 38 | watch 39 | ..reset() 40 | ..start(); 41 | final result = shortestPath(0, size - 1, (e) => graph[e] ?? [])!; 42 | final length = result.length; 43 | final first = result.first; 44 | watch.stop(); 45 | assert(length == 999, '$length'); 46 | assert(first == 1, '$first'); 47 | 48 | if (minTicks == null || watch.elapsedTicks < minTicks) { 49 | minTicks = watch.elapsedTicks; 50 | maxIteration = i; 51 | } 52 | 53 | if (maxIteration == i || (i - maxIteration) % 100000 == 0) { 54 | print('min ticks for one run: $minTicks\t' 55 | 'after $maxIteration of $i iterations'); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.3.0 2 | 3 | - Add a `transitiveClosure` function. 4 | - Make `stronglyConnectedComponents` and `topologicalSort` iterative rather than 5 | recursive to avoid stack overflows on very large graphs. 6 | - Require Dart 2.18 7 | 8 | # 2.2.0 9 | 10 | - Add a `secondarySort` parameter to the `topologicalSort()` function which 11 | applies an additional lexical sort where that doesn't break the topological 12 | sort. 13 | 14 | # 2.1.0 15 | 16 | - Add a `topologicalSort()` function. 17 | 18 | # 2.0.0 19 | 20 | - **Breaking**: `crawlAsync` will no longer ignore a node from the graph if the 21 | `readNode` callback returns null. 22 | 23 | # 1.0.0 24 | 25 | - Migrate to null safety. 26 | - **Breaking**: Paths from `shortestPath[s]` are now returned as iterables to 27 | reduce memory consumption of the algorithm to O(n). 28 | 29 | # 0.2.0 30 | 31 | - **BREAKING** `shortestPath`, `shortestPaths` and `stronglyConnectedComponents` 32 | now have one generic parameter and have replaced the `key` parameter with 33 | optional params: `{bool equals(T key1, T key2), int hashCode(T key)}`. 34 | This follows the pattern used in `dart:collection` classes `HashMap` and 35 | `LinkedHashMap`. It improves the usability and performance of the case where 36 | the source values are directly usable in a hash data structure. 37 | 38 | # 0.1.3+1 39 | 40 | - Fixed a bug with non-identity `key` in `shortestPath` and `shortestPaths`. 41 | 42 | # 0.1.3 43 | 44 | - Added `shortestPath` and `shortestPaths` functions. 45 | - Use `HashMap` and `HashSet` from `dart:collection` for 46 | `stronglyConnectedComponents`. Improves runtime performance. 47 | 48 | # 0.1.2+1 49 | 50 | - Allow using non-dev Dart 2 SDK. 51 | 52 | # 0.1.2 53 | 54 | - `crawlAsync` surfaces exceptions while crawling through the result stream 55 | rather than as uncaught asynchronous errors. 56 | 57 | # 0.1.1 58 | 59 | - `crawlAsync` will now ignore nodes that are resolved to `null`. 60 | 61 | # 0.1.0 62 | 63 | - Initial release with an implementation of `stronglyConnectedComponents` and 64 | `crawlAsync`. 65 | -------------------------------------------------------------------------------- /lib/src/crawl_async.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, 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:async'; 6 | import 'dart:collection'; 7 | 8 | final _empty = Future.value(); 9 | 10 | /// Finds and returns every node in a graph who's nodes and edges are 11 | /// asynchronously resolved. 12 | /// 13 | /// Cycles are allowed. If this is an undirected graph the [edges] function 14 | /// may be symmetric. In this case the [roots] may be any node in each connected 15 | /// graph. 16 | /// 17 | /// [V] is the type of values in the graph nodes. [K] must be a type suitable 18 | /// for using as a Map or Set key. [edges] should return the next reachable 19 | /// nodes. 20 | /// 21 | /// There are no ordering guarantees. This is useful for ensuring some work is 22 | /// performed at every node in an asynchronous graph, but does not give 23 | /// guarantees that the work is done in topological order. 24 | /// 25 | /// If either [readNode] or [edges] throws the error will be forwarded 26 | /// through the result stream and no further nodes will be crawled, though some 27 | /// work may have already been started. 28 | /// 29 | /// Crawling is eager, so calls to [edges] may overlap with other calls that 30 | /// have not completed. If the [edges] callback needs to be limited or throttled 31 | /// that must be done by wrapping it before calling [crawlAsync]. 32 | Stream crawlAsync( 33 | Iterable roots, 34 | FutureOr Function(K) readNode, 35 | FutureOr> Function(K, V) edges, 36 | ) { 37 | final crawl = _CrawlAsync(roots, readNode, edges)..run(); 38 | return crawl.result.stream; 39 | } 40 | 41 | class _CrawlAsync { 42 | final result = StreamController(); 43 | 44 | final FutureOr Function(K) readNode; 45 | final FutureOr> Function(K, V) edges; 46 | final Iterable roots; 47 | 48 | final _seen = HashSet(); 49 | 50 | _CrawlAsync(this.roots, this.readNode, this.edges); 51 | 52 | /// Add all nodes in the graph to [result] and return a Future which fires 53 | /// after all nodes have been seen. 54 | Future run() async { 55 | try { 56 | await Future.wait(roots.map(_visit), eagerError: true); 57 | await result.close(); 58 | } catch (e, st) { 59 | result.addError(e, st); 60 | await result.close(); 61 | } 62 | } 63 | 64 | /// Resolve the node at [key] and output it, then start crawling all of it's 65 | /// edges. 66 | Future _crawlFrom(K key) async { 67 | final value = await readNode(key); 68 | if (result.isClosed) return; 69 | result.add(value); 70 | final next = await edges(key, value); 71 | await Future.wait(next.map(_visit), eagerError: true); 72 | } 73 | 74 | /// Synchronously record that [key] is being handled then start work on the 75 | /// node for [key]. 76 | /// 77 | /// The returned Future will complete only after the work for [key] and all 78 | /// transitively reachable nodes has either been finished, or will be finished 79 | /// by some other Future in [_seen]. 80 | Future _visit(K key) { 81 | if (_seen.contains(key)) return _empty; 82 | _seen.add(key); 83 | return _crawlFrom(key); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /example/crawl_async_example.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, 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:async'; 6 | import 'dart:isolate'; 7 | 8 | import 'package:analyzer/dart/analysis/analysis_context.dart'; 9 | import 'package:analyzer/dart/analysis/context_builder.dart'; 10 | import 'package:analyzer/dart/analysis/context_locator.dart'; 11 | import 'package:analyzer/dart/analysis/results.dart'; 12 | import 'package:analyzer/dart/ast/ast.dart'; 13 | import 'package:graphs/graphs.dart'; 14 | import 'package:path/path.dart' as p; 15 | import 'package:pool/pool.dart'; 16 | 17 | /// Print a transitive set of imported URIs where libraries are read 18 | /// asynchronously. 19 | Future main() async { 20 | // Limits calls to [findImports]. 21 | final pool = Pool(10); 22 | final allImports = await crawlAsync( 23 | [Uri.parse('package:graphs/graphs.dart')], 24 | read, 25 | (from, source) => pool.withResource(() => findImports(from, source)), 26 | ).toList(); 27 | print(allImports.map((s) => s.uri).toList()); 28 | } 29 | 30 | AnalysisContext? _analysisContext; 31 | 32 | Future get analysisContext async { 33 | var context = _analysisContext; 34 | if (context == null) { 35 | final libUri = Uri.parse('package:graphs/'); 36 | final libPath = await pathForUri(libUri); 37 | final packagePath = p.dirname(libPath); 38 | 39 | final roots = ContextLocator().locateRoots(includedPaths: [packagePath]); 40 | if (roots.length != 1) { 41 | throw StateError('Expected to find exactly one context root, got $roots'); 42 | } 43 | 44 | context = _analysisContext = 45 | ContextBuilder().createContext(contextRoot: roots[0]); 46 | } 47 | 48 | return context; 49 | } 50 | 51 | Future> findImports(Uri from, Source source) async => 52 | source.unit.directives 53 | .whereType() 54 | .map((d) => d.uri.stringValue!) 55 | .where((uri) => !uri.startsWith('dart:')) 56 | .map((import) => resolveImport(import, from)); 57 | 58 | Future parseUri(Uri uri) async { 59 | final path = await pathForUri(uri); 60 | final analysisSession = (await analysisContext).currentSession; 61 | final parseResult = analysisSession.getParsedUnit(path); 62 | return (parseResult as ParsedUnitResult).unit; 63 | } 64 | 65 | Future pathForUri(Uri uri) async { 66 | final fileUri = await Isolate.resolvePackageUri(uri); 67 | if (fileUri == null || !fileUri.isScheme('file')) { 68 | throw StateError('Expected to resolve $uri to a file URI, got $fileUri'); 69 | } 70 | return p.fromUri(fileUri); 71 | } 72 | 73 | Future read(Uri uri) async => Source(uri, await parseUri(uri)); 74 | 75 | Uri resolveImport(String import, Uri from) { 76 | if (import.startsWith('package:')) return Uri.parse(import); 77 | assert(from.scheme == 'package'); 78 | final package = from.pathSegments.first; 79 | final fromPath = p.joinAll(from.pathSegments.skip(1)); 80 | final path = p.normalize(p.join(p.dirname(fromPath), import)); 81 | return Uri.parse('package:${p.join(package, path)}'); 82 | } 83 | 84 | class Source { 85 | final Uri uri; 86 | final CompilationUnit unit; 87 | 88 | Source(this.uri, this.unit); 89 | } 90 | -------------------------------------------------------------------------------- /test/crawl_async_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, 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 'package:graphs/graphs.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import 'utils/graph.dart'; 9 | 10 | void main() { 11 | group('asyncCrawl', () { 12 | Future> crawl( 13 | Map?> g, 14 | Iterable roots, 15 | ) { 16 | final graph = AsyncGraph(g); 17 | return crawlAsync(roots, graph.readNode, graph.edges).toList(); 18 | } 19 | 20 | test('empty result for empty graph', () async { 21 | final result = await crawl({}, []); 22 | expect(result, isEmpty); 23 | }); 24 | 25 | test('single item for a single node', () async { 26 | final result = await crawl({'a': []}, ['a']); 27 | expect(result, ['a']); 28 | }); 29 | 30 | test('hits every node in a graph', () async { 31 | final result = await crawl({ 32 | 'a': ['b', 'c'], 33 | 'b': ['c'], 34 | 'c': ['d'], 35 | 'd': [], 36 | }, [ 37 | 'a' 38 | ]); 39 | expect(result, hasLength(4)); 40 | expect( 41 | result, 42 | allOf(contains('a'), contains('b'), contains('c'), contains('d')), 43 | ); 44 | }); 45 | 46 | test('handles cycles', () async { 47 | final result = await crawl({ 48 | 'a': ['b'], 49 | 'b': ['c'], 50 | 'c': ['b'], 51 | }, [ 52 | 'a' 53 | ]); 54 | expect(result, hasLength(3)); 55 | expect(result, allOf(contains('a'), contains('b'), contains('c'))); 56 | }); 57 | 58 | test('handles self cycles', () async { 59 | final result = await crawl({ 60 | 'a': ['b'], 61 | 'b': ['b'], 62 | }, [ 63 | 'a' 64 | ]); 65 | expect(result, hasLength(2)); 66 | expect(result, allOf(contains('a'), contains('b'))); 67 | }); 68 | 69 | test('allows null edges', () async { 70 | final result = await crawl({ 71 | 'a': ['b'], 72 | 'b': null, 73 | }, [ 74 | 'a' 75 | ]); 76 | expect(result, hasLength(2)); 77 | expect(result, allOf(contains('a'), contains('b'))); 78 | }); 79 | 80 | test('allows null nodes', () async { 81 | final result = await crawl({ 82 | 'a': ['b'], 83 | }, [ 84 | 'a' 85 | ]); 86 | expect(result, ['a', null]); 87 | }); 88 | 89 | test('surfaces exceptions for crawling edges', () { 90 | final graph = { 91 | 'a': ['b'], 92 | }; 93 | final nodes = crawlAsync( 94 | ['a'], 95 | (n) => n, 96 | (k, n) => k == 'b' ? throw ArgumentError() : graph[k] ?? [], 97 | ); 98 | expect(nodes, emitsThrough(emitsError(isArgumentError))); 99 | }); 100 | 101 | test('surfaces exceptions for resolving keys', () { 102 | final graph = { 103 | 'a': ['b'], 104 | }; 105 | final nodes = crawlAsync( 106 | ['a'], 107 | (n) => n == 'b' ? throw ArgumentError() : n, 108 | (k, n) => graph[k] ?? [], 109 | ); 110 | expect(nodes, emitsThrough(emitsError(isArgumentError))); 111 | }); 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /lib/src/strongly_connected_components.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, 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 | import 'dart:math' show min; 7 | 8 | /// Finds the strongly connected components of a directed graph using Tarjan's 9 | /// algorithm. 10 | /// 11 | /// The result will be a valid reverse topological order ordering of the 12 | /// strongly connected components. Components further from a root will appear in 13 | /// the result before the components which they are connected to. 14 | /// 15 | /// Nodes within a strongly connected component have no ordering guarantees, 16 | /// except that if the first value in [nodes] is a valid root, and is contained 17 | /// in a cycle, it will be the last element of that cycle. 18 | /// 19 | /// [nodes] must contain at least a root of every tree in the graph if there are 20 | /// disjoint subgraphs but it may contain all nodes in the graph if the roots 21 | /// are not known. 22 | /// 23 | /// If [equals] is provided, it is used to compare nodes in the graph. If 24 | /// [equals] is omitted, the node's own [Object.==] is used instead. 25 | /// 26 | /// Similarly, if [hashCode] is provided, it is used to produce a hash value 27 | /// for nodes to efficiently calculate the return value. If it is omitted, the 28 | /// key's own [Object.hashCode] is used. 29 | /// 30 | /// If you supply one of [equals] or [hashCode], you should generally also to 31 | /// supply the other. 32 | List> stronglyConnectedComponents( 33 | Iterable nodes, 34 | Iterable Function(T) edges, { 35 | bool Function(T, T)? equals, 36 | int Function(T)? hashCode, 37 | }) { 38 | final result = >[]; 39 | final lowLinks = HashMap(equals: equals, hashCode: hashCode); 40 | final indexes = HashMap(equals: equals, hashCode: hashCode); 41 | final onStack = HashSet(equals: equals, hashCode: hashCode); 42 | 43 | final nonNullEquals = equals ?? _defaultEquals; 44 | 45 | var index = 0; 46 | final lastVisited = Queue(); 47 | 48 | final stack = [for (final node in nodes) _StackState(node)]; 49 | outer: 50 | while (stack.isNotEmpty) { 51 | final state = stack.removeLast(); 52 | final node = state.node; 53 | var iterator = state.iterator; 54 | 55 | int lowLink; 56 | if (iterator == null) { 57 | if (indexes.containsKey(node)) continue; 58 | indexes[node] = index; 59 | lowLink = lowLinks[node] = index; 60 | index++; 61 | iterator = edges(node).iterator; 62 | 63 | // Nodes with no edges are always in their own component. 64 | if (!iterator.moveNext()) { 65 | result.add([node]); 66 | continue; 67 | } 68 | 69 | lastVisited.addLast(node); 70 | onStack.add(node); 71 | } else { 72 | lowLink = min(lowLinks[node]!, lowLinks[iterator.current]!); 73 | } 74 | 75 | do { 76 | final next = iterator.current; 77 | if (!indexes.containsKey(next)) { 78 | stack.add(_StackState(node, iterator)); 79 | stack.add(_StackState(next)); 80 | continue outer; 81 | } else if (onStack.contains(next)) { 82 | lowLink = lowLinks[node] = min(lowLink, indexes[next]!); 83 | } 84 | } while (iterator.moveNext()); 85 | 86 | if (lowLink == indexes[node]) { 87 | final component = []; 88 | T next; 89 | do { 90 | next = lastVisited.removeLast(); 91 | onStack.remove(next); 92 | component.add(next); 93 | } while (!nonNullEquals(next, node)); 94 | result.add(component); 95 | } 96 | } 97 | 98 | return result; 99 | } 100 | 101 | /// The state of a pass on a single node in Tarjan's Algorithm. 102 | /// 103 | /// This is used to perform the algorithm with an explicit stack rather than 104 | /// recursively, to avoid stack overflow errors for very large graphs. 105 | class _StackState { 106 | /// The node being inspected. 107 | final T node; 108 | 109 | /// The iterator traversing [node]'s edges. 110 | /// 111 | /// This is null if the node hasn't yet begun being traversed. 112 | final Iterator? iterator; 113 | 114 | _StackState(this.node, [this.iterator]); 115 | } 116 | 117 | bool _defaultEquals(Object a, Object b) => a == b; 118 | -------------------------------------------------------------------------------- /lib/src/shortest_path.dart: -------------------------------------------------------------------------------- 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 | /// Returns the shortest path from [start] to [target] given the directed 8 | /// edges of a graph provided by [edges]. 9 | /// 10 | /// If [start] `==` [target], an empty [List] is returned and [edges] is never 11 | /// called. 12 | /// 13 | /// If [equals] is provided, it is used to compare nodes in the graph. If 14 | /// [equals] is omitted, the node's own [Object.==] is used instead. 15 | /// 16 | /// Similarly, if [hashCode] is provided, it is used to produce a hash value 17 | /// for nodes to efficiently calculate the return value. If it is omitted, the 18 | /// key's own [Object.hashCode] is used. 19 | /// 20 | /// If you supply one of [equals] or [hashCode], you should generally also to 21 | /// supply the other. 22 | Iterable? shortestPath( 23 | T start, 24 | T target, 25 | Iterable Function(T) edges, { 26 | bool Function(T, T)? equals, 27 | int Function(T)? hashCode, 28 | }) => 29 | _shortestPaths( 30 | start, 31 | edges, 32 | target: target, 33 | equals: equals, 34 | hashCode: hashCode, 35 | )[target]; 36 | 37 | /// Returns a [Map] of the shortest paths from [start] to all of the nodes in 38 | /// the directed graph defined by [edges]. 39 | /// 40 | /// All return values will contain the key [start] with an empty [List] value. 41 | /// 42 | /// [start] and all values returned by [edges] must not be `null`. 43 | /// If asserts are enabled, an [AssertionError] is raised if these conditions 44 | /// are not met. If asserts are not enabled, violations result in undefined 45 | /// behavior. 46 | /// 47 | /// If [equals] is provided, it is used to compare nodes in the graph. If 48 | /// [equals] is omitted, the node's own [Object.==] is used instead. 49 | /// 50 | /// Similarly, if [hashCode] is provided, it is used to produce a hash value 51 | /// for nodes to efficiently calculate the return value. If it is omitted, the 52 | /// key's own [Object.hashCode] is used. 53 | /// 54 | /// If you supply one of [equals] or [hashCode], you should generally also to 55 | /// supply the other. 56 | Map> shortestPaths( 57 | T start, 58 | Iterable Function(T) edges, { 59 | bool Function(T, T)? equals, 60 | int Function(T)? hashCode, 61 | }) => 62 | _shortestPaths( 63 | start, 64 | edges, 65 | equals: equals, 66 | hashCode: hashCode, 67 | ); 68 | 69 | Map> _shortestPaths( 70 | T start, 71 | Iterable Function(T) edges, { 72 | T? target, 73 | bool Function(T, T)? equals, 74 | int Function(T)? hashCode, 75 | }) { 76 | final distances = HashMap>(equals: equals, hashCode: hashCode); 77 | distances[start] = _Tail(); 78 | 79 | final nonNullEquals = equals ??= _defaultEquals; 80 | final isTarget = 81 | target == null ? _neverTarget : (T node) => nonNullEquals(node, target); 82 | if (isTarget(start)) { 83 | return distances; 84 | } 85 | 86 | final toVisit = ListQueue()..add(start); 87 | 88 | while (toVisit.isNotEmpty) { 89 | final current = toVisit.removeFirst(); 90 | final currentPath = distances[current]!; 91 | 92 | for (var edge in edges(current)) { 93 | final existingPath = distances[edge]; 94 | 95 | if (existingPath == null) { 96 | distances[edge] = currentPath.append(edge); 97 | if (isTarget(edge)) { 98 | return distances; 99 | } 100 | toVisit.add(edge); 101 | } 102 | } 103 | } 104 | 105 | return distances; 106 | } 107 | 108 | bool _defaultEquals(Object a, Object b) => a == b; 109 | bool _neverTarget(Object _) => false; 110 | 111 | /// An immutable iterable that can efficiently return a copy with a value 112 | /// appended. 113 | /// 114 | /// This implementation has an efficient [length] property. 115 | /// 116 | /// Note that grabbing an [iterator] for the first time is O(n) in time and 117 | /// space because it copies all the values to a new list and uses that 118 | /// iterator in order to avoid stack overflows for large paths. This copy is 119 | /// cached for subsequent calls. 120 | class _Tail extends Iterable { 121 | final T? tail; 122 | final _Tail? head; 123 | @override 124 | final int length; 125 | _Tail() 126 | : tail = null, 127 | head = null, 128 | length = 0; 129 | _Tail._(this.tail, this.head, this.length); 130 | _Tail append(T value) => _Tail._(value, this, length + 1); 131 | 132 | @override 133 | Iterator get iterator => _asIterable.iterator; 134 | 135 | late final _asIterable = () { 136 | _Tail? next = this; 137 | final reversed = List.generate(length, (_) { 138 | final val = next!.tail; 139 | next = next!.head; 140 | return val as T; 141 | }); 142 | return reversed.reversed; 143 | }(); 144 | } 145 | -------------------------------------------------------------------------------- /test/shortest_path_test.dart: -------------------------------------------------------------------------------- 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 | import 'dart:math' show Random; 7 | 8 | import 'package:graphs/graphs.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import 'utils/utils.dart'; 12 | 13 | void main() { 14 | const graph = >{ 15 | '1': ['2', '5'], 16 | '2': ['3'], 17 | '3': ['4', '5'], 18 | '4': ['1'], 19 | '5': ['8'], 20 | '6': ['7'], 21 | }; 22 | 23 | List readEdges(String key) => graph[key] ?? []; 24 | 25 | List getXValues(X key) => graph[key.value]?.map(X.new).toList() ?? []; 26 | 27 | void singlePathTest(String from, String to, List? expected) { 28 | test('$from -> $to should be $expected (mapped)', () { 29 | expect( 30 | shortestPath( 31 | X(from), 32 | X(to), 33 | getXValues, 34 | equals: xEquals, 35 | hashCode: xHashCode, 36 | )?.map((x) => x.value), 37 | expected, 38 | ); 39 | }); 40 | 41 | test('$from -> $to should be $expected', () { 42 | expect(shortestPath(from, to, readEdges), expected); 43 | }); 44 | } 45 | 46 | void pathsTest( 47 | String from, 48 | Map> expected, 49 | List nullPaths, 50 | ) { 51 | test('paths from $from (mapped)', () { 52 | final result = shortestPaths( 53 | X(from), 54 | getXValues, 55 | equals: xEquals, 56 | hashCode: xHashCode, 57 | ).map((k, v) => MapEntry(k.value, v.map((x) => x.value).toList())); 58 | expect(result, expected); 59 | }); 60 | 61 | test('paths from $from', () { 62 | final result = shortestPaths(from, readEdges); 63 | expect(result, expected); 64 | }); 65 | 66 | for (var entry in expected.entries) { 67 | singlePathTest(from, entry.key, entry.value); 68 | } 69 | 70 | for (var entry in nullPaths) { 71 | singlePathTest(from, entry, null); 72 | } 73 | } 74 | 75 | pathsTest('1', { 76 | '5': ['5'], 77 | '3': ['2', '3'], 78 | '8': ['5', '8'], 79 | '1': [], 80 | '2': ['2'], 81 | '4': ['2', '3', '4'], 82 | }, [ 83 | '6', 84 | '7', 85 | ]); 86 | 87 | pathsTest('6', { 88 | '7': ['7'], 89 | '6': [], 90 | }, [ 91 | '1', 92 | ]); 93 | pathsTest('7', {'7': []}, ['1', '6']); 94 | 95 | pathsTest('42', {'42': []}, ['1', '6']); 96 | 97 | test('integration test', () { 98 | // Be deterministic in the generated graph. This test may have to be updated 99 | // if the behavior of `Random` changes for the provided seed. 100 | final rnd = Random(1); 101 | const size = 1000; 102 | final graph = HashMap>(); 103 | 104 | Iterable? resultForGraph() => 105 | shortestPath(0, size - 1, (e) => graph[e] ?? const []); 106 | 107 | void addRandomEdge() { 108 | final toList = graph.putIfAbsent(rnd.nextInt(size), () => []); 109 | 110 | final toValue = rnd.nextInt(size); 111 | if (!toList.contains(toValue)) { 112 | toList.add(toValue); 113 | } 114 | } 115 | 116 | Iterable? result; 117 | 118 | // Add edges until there is a shortest path between `0` and `size - 1` 119 | do { 120 | addRandomEdge(); 121 | result = resultForGraph(); 122 | } while (result == null); 123 | 124 | expect(result, [313, 547, 91, 481, 74, 64, 439, 388, 660, 275, 999]); 125 | 126 | var count = 0; 127 | // Add edges until the shortest path between `0` and `size - 1` is 2 items 128 | // Adding edges should never increase the length of the shortest path. 129 | // Adding enough edges should reduce the length of the shortest path. 130 | do { 131 | expect(++count, lessThan(size * 5), reason: 'This loop should finish.'); 132 | addRandomEdge(); 133 | final previousResultLength = result!.length; 134 | result = resultForGraph(); 135 | expect(result, hasLength(lessThanOrEqualTo(previousResultLength))); 136 | } while (result!.length > 2); 137 | 138 | expect(result, [275, 999]); 139 | 140 | count = 0; 141 | // Remove edges until there is no shortest path. 142 | // Removing edges should never reduce the length of the shortest path. 143 | // Removing enough edges should increase the length of the shortest path and 144 | // eventually eliminate any path. 145 | do { 146 | expect(++count, lessThan(size * 5), reason: 'This loop should finish.'); 147 | final randomKey = graph.keys.elementAt(rnd.nextInt(graph.length)); 148 | final list = graph[randomKey]!; 149 | expect(list, isNotEmpty); 150 | list.removeAt(rnd.nextInt(list.length)); 151 | if (list.isEmpty) { 152 | graph.remove(randomKey); 153 | } 154 | final previousResultLength = result!.length; 155 | result = resultForGraph(); 156 | if (result != null) { 157 | expect(result, hasLength(greaterThanOrEqualTo(previousResultLength))); 158 | } 159 | } while (result != null); 160 | }); 161 | } 162 | -------------------------------------------------------------------------------- /lib/src/transitive_closure.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, 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 'cycle_exception.dart'; 8 | import 'strongly_connected_components.dart'; 9 | import 'topological_sort.dart'; 10 | 11 | /// Returns a transitive closure of a directed graph provided by [nodes] and 12 | /// [edges]. 13 | /// 14 | /// The result is a map from [nodes] to the sets of nodes that are transitively 15 | /// reachable through [edges]. No particular ordering is guaranteed. 16 | /// 17 | /// If [equals] is provided, it is used to compare nodes in the graph. If 18 | /// [equals] is omitted, the node's own [Object.==] is used instead. 19 | /// 20 | /// Similarly, if [hashCode] is provided, it is used to produce a hash value 21 | /// for nodes to efficiently calculate the return value. If it is omitted, the 22 | /// key's own [Object.hashCode] is used. 23 | /// 24 | /// If you supply one of [equals] or [hashCode], you should generally also to 25 | /// supply the other. 26 | /// 27 | /// Note: this requires that [nodes] and each iterable returned by [edges] 28 | /// contain no duplicate entries. 29 | /// 30 | /// By default, this can handle either cyclic or acyclic graphs. If [acyclic] is 31 | /// true, this will run more efficiently but throw a [CycleException] if the 32 | /// graph is cyclical. 33 | Map> transitiveClosure( 34 | Iterable nodes, 35 | Iterable Function(T) edges, { 36 | bool Function(T, T)? equals, 37 | int Function(T)? hashCode, 38 | bool acyclic = false, 39 | }) { 40 | if (!acyclic) { 41 | return _cyclicTransitiveClosure( 42 | nodes, 43 | edges, 44 | equals: equals, 45 | hashCode: hashCode, 46 | ); 47 | } 48 | 49 | final topologicalOrder = 50 | topologicalSort(nodes, edges, equals: equals, hashCode: hashCode); 51 | final result = LinkedHashMap>(equals: equals, hashCode: hashCode); 52 | for (final node in topologicalOrder.reversed) { 53 | final closure = LinkedHashSet(equals: equals, hashCode: hashCode); 54 | for (var child in edges(node)) { 55 | closure.add(child); 56 | closure.addAll(result[child]!); 57 | } 58 | 59 | result[node] = closure; 60 | } 61 | 62 | return result; 63 | } 64 | 65 | /// Returns the transitive closure of a cyclic graph using [Purdom's algorithm]. 66 | /// 67 | /// [Purdom's algorithm]: https://algowiki-project.org/en/Purdom%27s_algorithm 68 | /// 69 | /// This first computes the strongly connected components of the graph and finds 70 | /// the transitive closure of those before flattening it out into the transitive 71 | /// closure of the entire graph. 72 | Map> _cyclicTransitiveClosure( 73 | Iterable nodes, 74 | Iterable Function(T) edges, { 75 | bool Function(T, T)? equals, 76 | int Function(T)? hashCode, 77 | }) { 78 | final components = stronglyConnectedComponents( 79 | nodes, 80 | edges, 81 | equals: equals, 82 | hashCode: hashCode, 83 | ); 84 | final nodesToComponents = 85 | HashMap>(equals: equals, hashCode: hashCode); 86 | for (final component in components) { 87 | for (final node in component) { 88 | nodesToComponents[node] = component; 89 | } 90 | } 91 | 92 | // Because [stronglyConnectedComponents] returns the components in reverse 93 | // topological order, we can avoid an additional topological sort here. 94 | // Instead, we directly traverse the component list with the knowledge that 95 | // once we reach a component, everything reachable from it has already been 96 | // registered in [result]. 97 | final result = LinkedHashMap>(equals: equals, hashCode: hashCode); 98 | for (final component in components) { 99 | final closure = LinkedHashSet(equals: equals, hashCode: hashCode); 100 | if (_componentIncludesCycle(component, edges, equals)) { 101 | closure.addAll(component); 102 | } 103 | 104 | // De-duplicate downstream components to avoid adding the same transitive 105 | // children over and over. 106 | final downstreamComponents = { 107 | for (final node in component) 108 | for (final child in edges(node)) nodesToComponents[child]! 109 | }; 110 | for (final childComponent in downstreamComponents) { 111 | if (childComponent == component) continue; 112 | 113 | // This if check is just for efficiency. If [childComponent] has multiple 114 | // nodes, `result[childComponent.first]` will contain all the nodes in 115 | // `childComponent` anyway since it's cyclical. 116 | if (childComponent.length == 1) closure.addAll(childComponent); 117 | closure.addAll(result[childComponent.first]!); 118 | } 119 | 120 | for (final node in component) { 121 | result[node] = closure; 122 | } 123 | } 124 | return result; 125 | } 126 | 127 | /// Returns whether the strongly-connected component [component] of a graph 128 | /// defined by [edges] includes a cycle. 129 | bool _componentIncludesCycle( 130 | List component, 131 | Iterable Function(T) edges, 132 | bool Function(T, T)? equals, 133 | ) { 134 | // A strongly-connected component with more than one node always contains a 135 | // cycle, by definition. 136 | if (component.length > 1) return true; 137 | 138 | // A component with only a single node only contains a cycle if that node has 139 | // an edge to itself. 140 | final node = component.single; 141 | return edges(node) 142 | .any((edge) => equals == null ? edge == node : equals(edge, node)); 143 | } 144 | -------------------------------------------------------------------------------- /lib/src/topological_sort.dart: -------------------------------------------------------------------------------- 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' hide stronglyConnectedComponents; 8 | 9 | import 'cycle_exception.dart'; 10 | 11 | /// Returns a topological sort of the nodes of the directed edges of a graph 12 | /// provided by [nodes] and [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 | /// If you supply [secondarySort], the resulting list will be sorted by that 31 | /// comparison function as much as possible without violating the topological 32 | /// ordering. Note that even with a secondary sort, the result is _still_ not 33 | /// guaranteed to be unique or stable across releases of this package. 34 | /// 35 | /// Note: this requires that [nodes] and each iterable returned by [edges] 36 | /// contain no duplicate entries. 37 | /// 38 | /// Throws a [CycleException] if the graph is cyclical. 39 | List topologicalSort( 40 | Iterable nodes, 41 | Iterable Function(T) edges, { 42 | bool Function(T, T)? equals, 43 | int Function(T)? hashCode, 44 | Comparator? secondarySort, 45 | }) { 46 | if (secondarySort != null) { 47 | return _topologicalSortWithSecondary( 48 | [...nodes], 49 | edges, 50 | secondarySort, 51 | equals, 52 | hashCode, 53 | ); 54 | } 55 | 56 | // https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search 57 | final result = QueueList(); 58 | final permanentMark = HashSet(equals: equals, hashCode: hashCode); 59 | final temporaryMark = LinkedHashSet(equals: equals, hashCode: hashCode); 60 | final stack = [...nodes]; 61 | while (stack.isNotEmpty) { 62 | final node = stack.removeLast(); 63 | if (permanentMark.contains(node)) continue; 64 | 65 | // If we're visiting this node while it's already marked and not through a 66 | // dependency, that must mean we've traversed all its dependencies and it's 67 | // safe to add it to the result. 68 | if (temporaryMark.contains(node)) { 69 | temporaryMark.remove(node); 70 | permanentMark.add(node); 71 | result.addFirst(node); 72 | } else { 73 | temporaryMark.add(node); 74 | 75 | // Revisit this node once we've visited all its children. 76 | stack.add(node); 77 | for (var child in edges(node)) { 78 | if (temporaryMark.contains(child)) throw CycleException(temporaryMark); 79 | stack.add(child); 80 | } 81 | } 82 | } 83 | 84 | return result; 85 | } 86 | 87 | /// An implementation of [topologicalSort] with a secondary comparison function. 88 | List _topologicalSortWithSecondary( 89 | List nodes, 90 | Iterable Function(T) edges, 91 | Comparator comparator, 92 | bool Function(T, T)? equals, 93 | int Function(T)? hashCode, 94 | ) { 95 | // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm, 96 | // modified to sort the nodes to traverse. Also documented in 97 | // https://www.algotree.org/algorithms/tree_graph_traversal/lexical_topological_sort_c++/ 98 | 99 | // For each node, the number of incoming edges it has that we haven't yet 100 | // traversed. 101 | final incomingEdges = HashMap(equals: equals, hashCode: hashCode); 102 | for (var node in nodes) { 103 | for (var child in edges(node)) { 104 | incomingEdges[child] = (incomingEdges[child] ?? 0) + 1; 105 | } 106 | } 107 | 108 | // A priority queue of nodes that have no remaining incoming edges. 109 | final nodesToTraverse = PriorityQueue(comparator); 110 | for (var node in nodes) { 111 | if (!incomingEdges.containsKey(node)) nodesToTraverse.add(node); 112 | } 113 | 114 | final result = []; 115 | while (nodesToTraverse.isNotEmpty) { 116 | final node = nodesToTraverse.removeFirst(); 117 | result.add(node); 118 | for (var child in edges(node)) { 119 | var remainingEdges = incomingEdges[child]!; 120 | remainingEdges--; 121 | incomingEdges[child] = remainingEdges; 122 | if (remainingEdges == 0) nodesToTraverse.add(child); 123 | } 124 | } 125 | 126 | if (result.length < nodes.length) { 127 | // This algorithm doesn't automatically produce a cycle list as a side 128 | // effect of sorting, so to throw the appropriate [CycleException] we just 129 | // call the normal [topologicalSort] with a view of this graph that only 130 | // includes nodes that still have edges. 131 | bool nodeIsInCycle(T node) { 132 | final edges = incomingEdges[node]; 133 | return edges != null && edges > 0; 134 | } 135 | 136 | topologicalSort( 137 | nodes.where(nodeIsInCycle), 138 | edges, 139 | equals: equals, 140 | hashCode: hashCode, 141 | ); 142 | assert(false, 'topologicalSort() should throw if the graph has a cycle'); 143 | } 144 | 145 | return result; 146 | } 147 | -------------------------------------------------------------------------------- /test/strongly_connected_components_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, 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 'package:graphs/graphs.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import 'utils/graph.dart'; 9 | import 'utils/utils.dart'; 10 | 11 | void main() { 12 | group('strongly connected components', () { 13 | /// Run [stronglyConnectedComponents] on [g]. 14 | List> components( 15 | Map?> g, { 16 | Iterable? startNodes, 17 | }) { 18 | final graph = Graph(g); 19 | return stronglyConnectedComponents( 20 | startNodes ?? graph.allNodes, 21 | graph.edges, 22 | ); 23 | } 24 | 25 | test('empty result for empty graph', () { 26 | final result = components({}); 27 | expect(result, isEmpty); 28 | }); 29 | 30 | test('single item for single node', () { 31 | final result = components({'a': []}); 32 | expect(result, [ 33 | ['a'] 34 | ]); 35 | }); 36 | 37 | test('handles non-cycles', () { 38 | final result = components({ 39 | 'a': ['b'], 40 | 'b': ['c'], 41 | 'c': [] 42 | }); 43 | expect(result, [ 44 | ['c'], 45 | ['b'], 46 | ['a'] 47 | ]); 48 | }); 49 | 50 | test('handles entire graph as cycle', () { 51 | final result = components({ 52 | 'a': ['b'], 53 | 'b': ['c'], 54 | 'c': ['d'], 55 | 'd': ['a'], 56 | }); 57 | expect( 58 | result, 59 | [allOf(contains('a'), contains('b'), contains('c'), contains('d'))], 60 | ); 61 | }); 62 | 63 | test('includes the first passed root last in a cycle', () { 64 | // In cases where this is used to find a topological ordering the first 65 | // value in nodes should always come last. 66 | final graph = { 67 | 'a': ['b'], 68 | 'b': ['a'] 69 | }; 70 | final resultFromA = components(graph, startNodes: ['a']); 71 | final resultFromB = components(graph, startNodes: ['b']); 72 | expect(resultFromA.single.last, 'a'); 73 | expect(resultFromB.single.last, 'b'); 74 | }); 75 | 76 | test('handles cycles in the middle', () { 77 | final result = components({ 78 | 'a': ['b', 'c'], 79 | 'b': ['c', 'd'], 80 | 'c': ['b', 'd'], 81 | 'd': [], 82 | }); 83 | expect(result, [ 84 | ['d'], 85 | allOf(contains('b'), contains('c')), 86 | ['a'], 87 | ]); 88 | }); 89 | 90 | test('handles self cycles', () { 91 | final result = components({ 92 | 'a': ['b'], 93 | 'b': ['b'], 94 | }); 95 | expect(result, [ 96 | ['b'], 97 | ['a'], 98 | ]); 99 | }); 100 | 101 | test('valid topological ordering for disjoint subgraphs', () { 102 | final result = components({ 103 | 'a': ['b', 'c'], 104 | 'b': ['b1', 'b2'], 105 | 'c': ['c1', 'c2'], 106 | 'b1': [], 107 | 'b2': [], 108 | 'c1': [], 109 | 'c2': [] 110 | }); 111 | 112 | expect( 113 | result, 114 | containsAllInOrder([ 115 | ['c1'], 116 | ['c'], 117 | ['a'] 118 | ]), 119 | ); 120 | expect( 121 | result, 122 | containsAllInOrder([ 123 | ['c2'], 124 | ['c'], 125 | ['a'] 126 | ]), 127 | ); 128 | expect( 129 | result, 130 | containsAllInOrder([ 131 | ['b1'], 132 | ['b'], 133 | ['a'] 134 | ]), 135 | ); 136 | expect( 137 | result, 138 | containsAllInOrder([ 139 | ['b2'], 140 | ['b'], 141 | ['a'] 142 | ]), 143 | ); 144 | }); 145 | 146 | test('handles getting null for edges', () { 147 | final result = components({ 148 | 'a': ['b'], 149 | 'b': null, 150 | }); 151 | expect(result, [ 152 | ['b'], 153 | ['a'] 154 | ]); 155 | }); 156 | }); 157 | 158 | group('custom hashCode and equals', () { 159 | /// Run [stronglyConnectedComponents] on [g]. 160 | List> components( 161 | Map?> g, { 162 | Iterable? startNodes, 163 | }) { 164 | final graph = BadGraph(g); 165 | 166 | startNodes ??= graph.allNodes.map((n) => n.value); 167 | 168 | return stronglyConnectedComponents( 169 | startNodes.map(X.new), 170 | graph.edges, 171 | equals: xEquals, 172 | hashCode: xHashCode, 173 | ).map((list) => list.map((x) => x.value).toList()).toList(); 174 | } 175 | 176 | test('empty result for empty graph', () { 177 | final result = components({}); 178 | expect(result, isEmpty); 179 | }); 180 | 181 | test('single item for single node', () { 182 | final result = components({'a': []}); 183 | expect(result, [ 184 | ['a'] 185 | ]); 186 | }); 187 | 188 | test('handles non-cycles', () { 189 | final result = components({ 190 | 'a': ['b'], 191 | 'b': ['c'], 192 | 'c': [] 193 | }); 194 | expect(result, [ 195 | ['c'], 196 | ['b'], 197 | ['a'] 198 | ]); 199 | }); 200 | 201 | test('handles entire graph as cycle', () { 202 | final result = components({ 203 | 'a': ['b'], 204 | 'b': ['c'], 205 | 'c': ['a'] 206 | }); 207 | expect(result, [allOf(contains('a'), contains('b'), contains('c'))]); 208 | }); 209 | 210 | test('includes the first passed root last in a cycle', () { 211 | // In cases where this is used to find a topological ordering the first 212 | // value in nodes should always come last. 213 | final graph = { 214 | 'a': ['b'], 215 | 'b': ['a'] 216 | }; 217 | final resultFromA = components(graph, startNodes: ['a']); 218 | final resultFromB = components(graph, startNodes: ['b']); 219 | expect(resultFromA.single.last, 'a'); 220 | expect(resultFromB.single.last, 'b'); 221 | }); 222 | 223 | test('handles cycles in the middle', () { 224 | final result = components({ 225 | 'a': ['b', 'c'], 226 | 'b': ['c', 'd'], 227 | 'c': ['b', 'd'], 228 | 'd': [], 229 | }); 230 | expect(result, [ 231 | ['d'], 232 | allOf(contains('b'), contains('c')), 233 | ['a'], 234 | ]); 235 | }); 236 | 237 | test('handles self cycles', () { 238 | final result = components({ 239 | 'a': ['b'], 240 | 'b': ['b'], 241 | }); 242 | expect(result, [ 243 | ['b'], 244 | ['a'], 245 | ]); 246 | }); 247 | 248 | test('valid topological ordering for disjoint subgraphs', () { 249 | final result = components({ 250 | 'a': ['b', 'c'], 251 | 'b': ['b1', 'b2'], 252 | 'c': ['c1', 'c2'], 253 | 'b1': [], 254 | 'b2': [], 255 | 'c1': [], 256 | 'c2': [] 257 | }); 258 | 259 | expect( 260 | result, 261 | containsAllInOrder([ 262 | ['c1'], 263 | ['c'], 264 | ['a'] 265 | ]), 266 | ); 267 | expect( 268 | result, 269 | containsAllInOrder([ 270 | ['c2'], 271 | ['c'], 272 | ['a'] 273 | ]), 274 | ); 275 | expect( 276 | result, 277 | containsAllInOrder([ 278 | ['b1'], 279 | ['b'], 280 | ['a'] 281 | ]), 282 | ); 283 | expect( 284 | result, 285 | containsAllInOrder([ 286 | ['b2'], 287 | ['b'], 288 | ['a'] 289 | ]), 290 | ); 291 | }); 292 | 293 | test('handles getting null for edges', () { 294 | final result = components({ 295 | 'a': ['b'], 296 | 'b': null, 297 | }); 298 | expect(result, [ 299 | ['b'], 300 | ['a'] 301 | ]); 302 | }); 303 | }); 304 | } 305 | -------------------------------------------------------------------------------- /test/transitive_closure_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, 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('for an acyclic graph', () { 14 | for (final acyclic in [true, false]) { 15 | group('with acyclic: $acyclic', () { 16 | group('returns the transitive closure for a graph', () { 17 | test('with no nodes', () { 18 | expect(_transitiveClosure({}, acyclic: acyclic), isEmpty); 19 | }); 20 | 21 | test('with only one node', () { 22 | expect( 23 | _transitiveClosure({1: []}, acyclic: acyclic), 24 | equals({1: {}}), 25 | ); 26 | }); 27 | 28 | test('with no edges', () { 29 | expect( 30 | _transitiveClosure( 31 | {1: [], 2: [], 3: [], 4: []}, 32 | acyclic: acyclic, 33 | ), 34 | equals(>{1: {}, 2: {}, 3: {}, 4: {}}), 35 | ); 36 | }); 37 | 38 | test('with single edges', () { 39 | expect( 40 | _transitiveClosure( 41 | { 42 | 1: [2], 43 | 2: [3], 44 | 3: [4], 45 | 4: [] 46 | }, 47 | acyclic: acyclic, 48 | ), 49 | equals({ 50 | 1: {2, 3, 4}, 51 | 2: {3, 4}, 52 | 3: {4}, 53 | 4: {} 54 | }), 55 | ); 56 | }); 57 | 58 | test('with many edges from one node', () { 59 | expect( 60 | _transitiveClosure( 61 | { 62 | 1: [2, 3, 4], 63 | 2: [], 64 | 3: [], 65 | 4: [] 66 | }, 67 | acyclic: acyclic, 68 | ), 69 | equals(>{ 70 | 1: {2, 3, 4}, 71 | 2: {}, 72 | 3: {}, 73 | 4: {} 74 | }), 75 | ); 76 | }); 77 | 78 | test('with transitive edges', () { 79 | expect( 80 | _transitiveClosure( 81 | { 82 | 1: [2, 4], 83 | 2: [], 84 | 3: [], 85 | 4: [3] 86 | }, 87 | acyclic: acyclic, 88 | ), 89 | equals(>{ 90 | 1: {2, 3, 4}, 91 | 2: {}, 92 | 3: {}, 93 | 4: {3} 94 | }), 95 | ); 96 | }); 97 | 98 | test('with diamond edges', () { 99 | expect( 100 | _transitiveClosure( 101 | { 102 | 1: [2, 3], 103 | 2: [4], 104 | 3: [4], 105 | 4: [] 106 | }, 107 | acyclic: acyclic, 108 | ), 109 | equals(>{ 110 | 1: {2, 3, 4}, 111 | 2: {4}, 112 | 3: {4}, 113 | 4: {} 114 | }), 115 | ); 116 | }); 117 | 118 | test('with disjoint subgraphs', () { 119 | expect( 120 | _transitiveClosure( 121 | { 122 | 1: [2], 123 | 2: [3], 124 | 3: [], 125 | 4: [5], 126 | 5: [6], 127 | 6: [], 128 | }, 129 | acyclic: acyclic, 130 | ), 131 | equals(>{ 132 | 1: {2, 3}, 133 | 2: {3}, 134 | 3: {}, 135 | 4: {5, 6}, 136 | 5: {6}, 137 | 6: {}, 138 | }), 139 | ); 140 | }); 141 | }); 142 | 143 | test('respects custom equality and hash functions', () { 144 | final result = _transitiveClosure( 145 | { 146 | 0: [2], 147 | 3: [4], 148 | 5: [6], 149 | 7: [] 150 | }, 151 | equals: (i, j) => (i ~/ 2) == (j ~/ 2), 152 | hashCode: (i) => (i ~/ 2).hashCode, 153 | ); 154 | 155 | expect( 156 | result.keys, 157 | unorderedMatches([ 158 | 0, 159 | anyOf([2, 3]), 160 | anyOf([4, 5]), 161 | anyOf([6, 7]) 162 | ]), 163 | ); 164 | expect( 165 | result[0], 166 | equals({ 167 | anyOf([2, 3]), 168 | anyOf([4, 5]), 169 | anyOf([6, 7]) 170 | }), 171 | ); 172 | expect( 173 | result[2], 174 | equals({ 175 | anyOf([4, 5]), 176 | anyOf([6, 7]) 177 | }), 178 | ); 179 | expect( 180 | result[4], 181 | equals({ 182 | anyOf([6, 7]) 183 | }), 184 | ); 185 | expect(result[6], isEmpty); 186 | }); 187 | }); 188 | } 189 | }); 190 | 191 | group('for a cyclic graph', () { 192 | group('with acyclic: true throws a CycleException for a graph with', () { 193 | test('a one-node cycle', () { 194 | expect( 195 | () => _transitiveClosure( 196 | { 197 | 1: [1] 198 | }, 199 | acyclic: true, 200 | ), 201 | throwsCycleException([1]), 202 | ); 203 | }); 204 | 205 | test('a multi-node cycle', () { 206 | expect( 207 | () => _transitiveClosure( 208 | { 209 | 1: [2], 210 | 2: [3], 211 | 3: [4], 212 | 4: [1] 213 | }, 214 | acyclic: true, 215 | ), 216 | throwsCycleException([4, 1, 2, 3]), 217 | ); 218 | }); 219 | }); 220 | 221 | group('returns the transitive closure for a graph', () { 222 | test('with a single one-node component', () { 223 | expect( 224 | _transitiveClosure({ 225 | 1: [1] 226 | }), 227 | equals({ 228 | 1: {1} 229 | }), 230 | ); 231 | }); 232 | 233 | test('with a single multi-node component', () { 234 | expect( 235 | _transitiveClosure({ 236 | 1: [2], 237 | 2: [3], 238 | 3: [4], 239 | 4: [1] 240 | }), 241 | equals({ 242 | 1: {1, 2, 3, 4}, 243 | 2: {1, 2, 3, 4}, 244 | 3: {1, 2, 3, 4}, 245 | 4: {1, 2, 3, 4} 246 | }), 247 | ); 248 | }); 249 | 250 | test('with a series of multi-node components', () { 251 | expect( 252 | _transitiveClosure({ 253 | 1: [2], 254 | 2: [1, 3], 255 | 3: [4], 256 | 4: [3, 5], 257 | 5: [6], 258 | 6: [5, 7], 259 | 7: [8], 260 | 8: [7], 261 | }), 262 | equals({ 263 | 1: {1, 2, 3, 4, 5, 6, 7, 8}, 264 | 2: {1, 2, 3, 4, 5, 6, 7, 8}, 265 | 3: {3, 4, 5, 6, 7, 8}, 266 | 4: {3, 4, 5, 6, 7, 8}, 267 | 5: {5, 6, 7, 8}, 268 | 6: {5, 6, 7, 8}, 269 | 7: {7, 8}, 270 | 8: {7, 8} 271 | }), 272 | ); 273 | }); 274 | 275 | test('with a diamond of multi-node components', () { 276 | expect( 277 | _transitiveClosure({ 278 | 1: [2], 279 | 2: [1, 3, 5], 280 | 3: [4], 281 | 4: [3, 7], 282 | 5: [6], 283 | 6: [5, 7], 284 | 7: [8], 285 | 8: [7], 286 | }), 287 | equals({ 288 | 1: {1, 2, 3, 4, 5, 6, 7, 8}, 289 | 2: {1, 2, 3, 4, 5, 6, 7, 8}, 290 | 3: {3, 4, 7, 8}, 291 | 4: {3, 4, 7, 8}, 292 | 5: {5, 6, 7, 8}, 293 | 6: {5, 6, 7, 8}, 294 | 7: {7, 8}, 295 | 8: {7, 8} 296 | }), 297 | ); 298 | }); 299 | 300 | test('mixed single- and multi-node components', () { 301 | expect( 302 | _transitiveClosure({ 303 | 1: [2], 304 | 2: [1, 3], 305 | 3: [4], 306 | 4: [5], 307 | 5: [4, 6], 308 | 6: [7], 309 | 7: [8], 310 | 8: [7], 311 | }), 312 | equals({ 313 | 1: {1, 2, 3, 4, 5, 6, 7, 8}, 314 | 2: {1, 2, 3, 4, 5, 6, 7, 8}, 315 | 3: {4, 5, 6, 7, 8}, 316 | 4: {4, 5, 6, 7, 8}, 317 | 5: {4, 5, 6, 7, 8}, 318 | 6: {7, 8}, 319 | 7: {7, 8}, 320 | 8: {7, 8} 321 | }), 322 | ); 323 | }); 324 | }); 325 | }); 326 | } 327 | 328 | /// Returns the transitive closure of a graph represented a map from keys to 329 | /// edges. 330 | Map> _transitiveClosure( 331 | Map> graph, { 332 | bool Function(T, T)? equals, 333 | int Function(T)? hashCode, 334 | bool acyclic = false, 335 | }) { 336 | assert((equals == null) == (hashCode == null)); 337 | if (equals != null) { 338 | graph = LinkedHashMap(equals: equals, hashCode: hashCode)..addAll(graph); 339 | } 340 | return transitiveClosure( 341 | graph.keys, 342 | (node) { 343 | expect(graph, contains(node)); 344 | return graph[node]!; 345 | }, 346 | equals: equals, 347 | hashCode: hashCode, 348 | acyclic: acyclic, 349 | ); 350 | } 351 | -------------------------------------------------------------------------------- /test/topological_sort_test.dart: -------------------------------------------------------------------------------- 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('without secondarySort', () { 14 | group('topologically sorts a graph', () { 15 | test('with no nodes', () { 16 | expect(_topologicalSort({}), isEmpty); 17 | }); 18 | 19 | test('with only one node', () { 20 | expect(_topologicalSort({1: []}), equals([1])); 21 | }); 22 | 23 | test('with no edges', () { 24 | expect( 25 | _topologicalSort({1: [], 2: [], 3: [], 4: []}), 26 | unorderedEquals([1, 2, 3, 4]), 27 | ); 28 | }); 29 | 30 | test('with single edges', () { 31 | expect( 32 | _topologicalSort({ 33 | 1: [2], 34 | 2: [3], 35 | 3: [4], 36 | 4: [] 37 | }), 38 | equals([1, 2, 3, 4]), 39 | ); 40 | }); 41 | 42 | test('with many edges from one node', () { 43 | final result = _topologicalSort({ 44 | 1: [2, 3, 4], 45 | 2: [], 46 | 3: [], 47 | 4: [] 48 | }); 49 | expect(result.indexOf(1), lessThan(result.indexOf(2))); 50 | expect(result.indexOf(1), lessThan(result.indexOf(3))); 51 | expect(result.indexOf(1), lessThan(result.indexOf(4))); 52 | }); 53 | 54 | test('with transitive edges', () { 55 | final result = _topologicalSort({ 56 | 1: [2, 4], 57 | 2: [], 58 | 3: [], 59 | 4: [3] 60 | }); 61 | expect(result.indexOf(1), lessThan(result.indexOf(2))); 62 | expect(result.indexOf(1), lessThan(result.indexOf(3))); 63 | expect(result.indexOf(1), lessThan(result.indexOf(4))); 64 | expect(result.indexOf(4), lessThan(result.indexOf(3))); 65 | }); 66 | 67 | test('with diamond edges', () { 68 | final result = _topologicalSort({ 69 | 1: [2, 3], 70 | 2: [4], 71 | 3: [4], 72 | 4: [] 73 | }); 74 | expect(result.indexOf(1), lessThan(result.indexOf(2))); 75 | expect(result.indexOf(1), lessThan(result.indexOf(3))); 76 | expect(result.indexOf(1), lessThan(result.indexOf(4))); 77 | expect(result.indexOf(2), lessThan(result.indexOf(4))); 78 | expect(result.indexOf(3), lessThan(result.indexOf(4))); 79 | }); 80 | }); 81 | 82 | test('respects custom equality and hash functions', () { 83 | expect( 84 | _topologicalSort( 85 | { 86 | 0: [2], 87 | 3: [4], 88 | 5: [6], 89 | 7: [] 90 | }, 91 | equals: (i, j) => (i ~/ 2) == (j ~/ 2), 92 | hashCode: (i) => (i ~/ 2).hashCode, 93 | ), 94 | equals([ 95 | 0, 96 | anyOf([2, 3]), 97 | anyOf([4, 5]), 98 | anyOf([6, 7]) 99 | ]), 100 | ); 101 | }); 102 | 103 | group('throws a CycleException for a graph with', () { 104 | test('a one-node cycle', () { 105 | expect( 106 | () => _topologicalSort({ 107 | 1: [1] 108 | }), 109 | throwsCycleException([1]), 110 | ); 111 | }); 112 | 113 | test('a multi-node cycle', () { 114 | expect( 115 | () => _topologicalSort({ 116 | 1: [2], 117 | 2: [3], 118 | 3: [4], 119 | 4: [1] 120 | }), 121 | throwsCycleException([4, 1, 2, 3]), 122 | ); 123 | }); 124 | }); 125 | }); 126 | 127 | group('with secondarySort', () { 128 | group('topologically sorts a graph', () { 129 | test('with no nodes', () { 130 | expect(_topologicalSort({}, secondarySort: true), isEmpty); 131 | }); 132 | 133 | test('with only one node', () { 134 | expect(_topologicalSort({1: []}, secondarySort: true), equals([1])); 135 | }); 136 | 137 | test('with no edges', () { 138 | expect( 139 | _topologicalSort({1: [], 2: [], 3: [], 4: []}, secondarySort: true), 140 | unorderedEquals([1, 2, 3, 4]), 141 | ); 142 | }); 143 | 144 | test('with single edges', () { 145 | expect( 146 | _topologicalSort( 147 | { 148 | 1: [2], 149 | 2: [3], 150 | 3: [4], 151 | 4: [] 152 | }, 153 | secondarySort: true, 154 | ), 155 | equals([1, 2, 3, 4]), 156 | ); 157 | }); 158 | 159 | test('with many edges from one node', () { 160 | final result = _topologicalSort( 161 | { 162 | 1: [2, 3, 4], 163 | 2: [], 164 | 3: [], 165 | 4: [] 166 | }, 167 | secondarySort: true, 168 | ); 169 | expect(result.indexOf(1), lessThan(result.indexOf(2))); 170 | expect(result.indexOf(1), lessThan(result.indexOf(3))); 171 | expect(result.indexOf(1), lessThan(result.indexOf(4))); 172 | }); 173 | 174 | test('with transitive edges', () { 175 | final result = _topologicalSort( 176 | { 177 | 1: [2, 4], 178 | 2: [], 179 | 3: [], 180 | 4: [3] 181 | }, 182 | secondarySort: true, 183 | ); 184 | expect(result.indexOf(1), lessThan(result.indexOf(2))); 185 | expect(result.indexOf(1), lessThan(result.indexOf(3))); 186 | expect(result.indexOf(1), lessThan(result.indexOf(4))); 187 | expect(result.indexOf(4), lessThan(result.indexOf(3))); 188 | }); 189 | 190 | test('with diamond edges', () { 191 | final result = _topologicalSort( 192 | { 193 | 1: [2, 3], 194 | 2: [4], 195 | 3: [4], 196 | 4: [] 197 | }, 198 | secondarySort: true, 199 | ); 200 | expect(result.indexOf(1), lessThan(result.indexOf(2))); 201 | expect(result.indexOf(1), lessThan(result.indexOf(3))); 202 | expect(result.indexOf(1), lessThan(result.indexOf(4))); 203 | expect(result.indexOf(2), lessThan(result.indexOf(4))); 204 | expect(result.indexOf(3), lessThan(result.indexOf(4))); 205 | }); 206 | }); 207 | 208 | group('lexically sorts a graph where possible', () { 209 | test('with no edges', () { 210 | final result = 211 | _topologicalSort({4: [], 3: [], 1: [], 2: []}, secondarySort: true); 212 | expect(result, equals([1, 2, 3, 4])); 213 | }); 214 | 215 | test('with one non-lexical edge', () { 216 | final result = _topologicalSort( 217 | { 218 | 4: [], 219 | 3: [1], 220 | 1: [], 221 | 2: [] 222 | }, 223 | secondarySort: true, 224 | ); 225 | expect( 226 | result, 227 | equals( 228 | anyOf([ 229 | [2, 3, 1, 4], 230 | [3, 1, 2, 4] 231 | ]), 232 | ), 233 | ); 234 | }); 235 | 236 | test('with a non-lexical topolgical order', () { 237 | final result = _topologicalSort( 238 | { 239 | 4: [3], 240 | 3: [2], 241 | 2: [1], 242 | 1: [] 243 | }, 244 | secondarySort: true, 245 | ); 246 | expect(result, equals([4, 3, 2, 1])); 247 | }); 248 | 249 | group('with multiple layers', () { 250 | test('in lexical order', () { 251 | final result = _topologicalSort( 252 | { 253 | 1: [2], 254 | 2: [3], 255 | 3: [], 256 | 4: [5], 257 | 5: [6], 258 | 6: [] 259 | }, 260 | secondarySort: true, 261 | ); 262 | expect(result, equals([1, 2, 3, 4, 5, 6])); 263 | }); 264 | 265 | test('in non-lexical order', () { 266 | final result = _topologicalSort( 267 | { 268 | 1: [3], 269 | 3: [5], 270 | 4: [2], 271 | 2: [6], 272 | 5: [], 273 | 6: [] 274 | }, 275 | secondarySort: true, 276 | ); 277 | expect( 278 | result, 279 | anyOf([ 280 | equals([1, 3, 4, 2, 5, 6]), 281 | equals([1, 4, 2, 3, 5, 6]) 282 | ]), 283 | ); 284 | }); 285 | }); 286 | }); 287 | 288 | test('respects custom equality and hash functions', () { 289 | expect( 290 | _topologicalSort( 291 | { 292 | 0: [2], 293 | 3: [4], 294 | 5: [6], 295 | 7: [] 296 | }, 297 | equals: (i, j) => (i ~/ 2) == (j ~/ 2), 298 | hashCode: (i) => (i ~/ 2).hashCode, 299 | secondarySort: true, 300 | ), 301 | equals([ 302 | 0, 303 | anyOf([2, 3]), 304 | anyOf([4, 5]), 305 | anyOf([6, 7]) 306 | ]), 307 | ); 308 | }); 309 | 310 | group('throws a CycleException for a graph with', () { 311 | test('a one-node cycle', () { 312 | expect( 313 | () => _topologicalSort( 314 | { 315 | 1: [1] 316 | }, 317 | secondarySort: true, 318 | ), 319 | throwsCycleException([1]), 320 | ); 321 | }); 322 | 323 | test('a multi-node cycle', () { 324 | expect( 325 | () => _topologicalSort( 326 | { 327 | 1: [2], 328 | 2: [3], 329 | 3: [4], 330 | 4: [1] 331 | }, 332 | secondarySort: true, 333 | ), 334 | throwsCycleException([4, 1, 2, 3]), 335 | ); 336 | }); 337 | }); 338 | }); 339 | } 340 | 341 | /// Runs a topological sort on a graph represented a map from keys to edges. 342 | List _topologicalSort( 343 | Map> graph, { 344 | bool Function(T, T)? equals, 345 | int Function(T)? hashCode, 346 | bool secondarySort = false, 347 | }) { 348 | if (equals != null) { 349 | graph = LinkedHashMap(equals: equals, hashCode: hashCode)..addAll(graph); 350 | } 351 | return topologicalSort( 352 | graph.keys, 353 | (node) { 354 | expect(graph, contains(node)); 355 | return graph[node]!; 356 | }, 357 | equals: equals, 358 | hashCode: hashCode, 359 | secondarySort: 360 | secondarySort ? (a, b) => (a as Comparable).compareTo(b) : null, 361 | ); 362 | } 363 | --------------------------------------------------------------------------------