├── .gitignore ├── .travis.yml ├── License ├── Makefile ├── README.md ├── component.json ├── graph.svg ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 6 5 | - 8 6 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | 2 | Toposort - Topological sorting for node.js 3 | Copyright (c) 2012 by Marcel Klehr 4 | MIT LICENSE 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | build: components index.js 3 | @component build --dev 4 | 5 | components: component.json 6 | @component install --dev 7 | 8 | clean: 9 | rm -fr build components template.js 10 | 11 | .PHONY: clean 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toposort 2 | 3 | Sort directed acyclic graphs 4 | 5 | [![Build Status](https://travis-ci.org/marcelklehr/toposort.png)](https://travis-ci.org/marcelklehr/toposort) 6 | 7 | ## Installation 8 | 9 | `npm install toposort` or `component install marcelklehr/toposort` 10 | 11 | then in your code: 12 | 13 | ```js 14 | toposort = require('toposort') 15 | ``` 16 | 17 | ## Usage 18 | We want to sort the following graph. 19 | 20 | ![graph](https://cdn.rawgit.com/marcelklehr/toposort/8b14e9fd/graph.svg) 21 | 22 | ```js 23 | // First, we define our edges. 24 | var graph = [ 25 | ['put on your shoes', 'tie your shoes'] 26 | , ['put on your shirt', 'put on your jacket'] 27 | , ['put on your shorts', 'put on your jacket'] 28 | , ['put on your shorts', 'put on your shoes'] 29 | ] 30 | 31 | 32 | // Now, sort the vertices topologically, to reveal a legal execution order. 33 | toposort(graph) 34 | // [ 'put on your shirt' 35 | // , 'put on your shorts' 36 | // , 'put on your jacket' 37 | // , 'put on your shoes' 38 | // , 'tie your shoes' ] 39 | ``` 40 | 41 | (Note that there is no defined order for graph parts that are not connected 42 | -- you could also put on your jacket after having tied your shoes...) 43 | 44 | ### Sorting dependencies 45 | It is usually more convenient to specify *dependencies* instead of "sequences". 46 | ```js 47 | // This time, edges represent dependencies. 48 | var graph = [ 49 | ['tie your shoes', 'put on your shoes'] 50 | , ['put on your jacket', 'put on your shirt'] 51 | , ['put on your shoes', 'put on your shorts'] 52 | , ['put on your jacket', 'put on your shorts'] 53 | ] 54 | 55 | toposort(graph) 56 | // [ 'tie your shoes' 57 | // , 'put on your shoes' 58 | // , 'put on your jacket' 59 | // , 'put on your shirt' 60 | // , 'put on your shorts' ] 61 | 62 | // Now, reversing the list will reveal a legal execution order. 63 | toposort(graph).reverse() 64 | // [ 'put on your shorts' 65 | // , 'put on your shirt' 66 | // , 'put on your jacket' 67 | // , 'put on your shoes' 68 | // , 'tie your shoes' ] 69 | ``` 70 | 71 | ## API 72 | 73 | ### toposort(edges) 74 | 75 | + edges {Array} An array of directed edges describing a graph. An edge looks like this: `[node1, node2]` (vertices needn't be strings but can be of any type). 76 | 77 | Returns: {Array} a list of vertices, sorted from "start" to "end" 78 | 79 | Throws an error if there are any cycles in the graph. 80 | 81 | ### toposort.array(nodes, edges) 82 | 83 | + nodes {Array} An array of nodes 84 | + edges {Array} An array of directed edges. You don't need to mention all `nodes` here. 85 | 86 | This is a convenience method that allows you to define nodes that may or may not be connected to any other nodes. The ordering of unconnected nodes is not defined. 87 | 88 | Returns: {Array} a list of vertices, sorted from "start" to "end" 89 | 90 | Throws an error if there are any cycles in the graph. 91 | 92 | ## Tests 93 | 94 | Run the tests with `node test.js`. 95 | 96 | ## Legal 97 | 98 | MIT License 99 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toposort", 3 | "author": "Marcel Klehr ", 4 | "repo": "marcelklehr/toposort", 5 | "description": "Topological sort of directed acyclic graphs (like dependency lists)", 6 | "version": "0.2.10", 7 | "keywords": [ 8 | "topological", 9 | "sort", 10 | "sorting", 11 | "graphs", 12 | "graph", 13 | "dependency", 14 | "list", 15 | "dependencies", 16 | "acyclic" 17 | ], 18 | "dependencies": {}, 19 | "development": {}, 20 | "license": "MIT", 21 | "scripts": [ 22 | "index.js" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /graph.svg: -------------------------------------------------------------------------------- 1 | image/svg+xmlput on your shortsput on your shirtput on your shoesput on your jackettie your shoes 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Topological sorting function 4 | * 5 | * @param {Array} edges 6 | * @returns {Array} 7 | */ 8 | 9 | module.exports = function(edges) { 10 | return toposort(uniqueNodes(edges), edges) 11 | } 12 | 13 | module.exports.array = toposort 14 | 15 | function toposort(nodes, edges) { 16 | var cursor = nodes.length 17 | , sorted = new Array(cursor) 18 | , visited = {} 19 | , i = cursor 20 | // Better data structures make algorithm much faster. 21 | , outgoingEdges = makeOutgoingEdges(edges) 22 | , nodesHash = makeNodesHash(nodes) 23 | 24 | // check for unknown nodes 25 | edges.forEach(function(edge) { 26 | if (!nodesHash.has(edge[0]) || !nodesHash.has(edge[1])) { 27 | throw new Error('Unknown node. There is an unknown node in the supplied edges.') 28 | } 29 | }) 30 | 31 | while (i--) { 32 | if (!visited[i]) visit(nodes[i], i, new Set()) 33 | } 34 | 35 | return sorted 36 | 37 | function visit(node, i, predecessors) { 38 | if(predecessors.has(node)) { 39 | var nodeRep 40 | try { 41 | nodeRep = ", node was:" + JSON.stringify(node) 42 | } catch(e) { 43 | nodeRep = "" 44 | } 45 | throw new Error('Cyclic dependency' + nodeRep) 46 | } 47 | 48 | if (!nodesHash.has(node)) { 49 | throw new Error('Found unknown node. Make sure to provided all involved nodes. Unknown node: '+JSON.stringify(node)) 50 | } 51 | 52 | if (visited[i]) return; 53 | visited[i] = true 54 | 55 | var outgoing = outgoingEdges.get(node) || new Set() 56 | outgoing = Array.from(outgoing) 57 | 58 | if (i = outgoing.length) { 59 | predecessors.add(node) 60 | do { 61 | var child = outgoing[--i] 62 | visit(child, nodesHash.get(child), predecessors) 63 | } while (i) 64 | predecessors.delete(node) 65 | } 66 | 67 | sorted[--cursor] = node 68 | } 69 | } 70 | 71 | function uniqueNodes(arr){ 72 | var res = new Set() 73 | for (var i = 0, len = arr.length; i < len; i++) { 74 | var edge = arr[i] 75 | res.add(edge[0]) 76 | res.add(edge[1]) 77 | } 78 | return Array.from(res) 79 | } 80 | 81 | function makeOutgoingEdges(arr){ 82 | var edges = new Map() 83 | for (var i = 0, len = arr.length; i < len; i++) { 84 | var edge = arr[i] 85 | if (!edges.has(edge[0])) edges.set(edge[0], new Set()) 86 | if (!edges.has(edge[1])) edges.set(edge[1], new Set()) 87 | edges.get(edge[0]).add(edge[1]) 88 | } 89 | return edges 90 | } 91 | 92 | function makeNodesHash(arr){ 93 | var res = new Map() 94 | for (var i = 0, len = arr.length; i < len; i++) { 95 | res.set(arr[i], i) 96 | } 97 | return res 98 | } 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toposort", 3 | "version": "2.0.2", 4 | "description": "Topological sort of directed ascyclic graphs (like dependecy lists)", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/marcelklehr/toposort.git" 12 | }, 13 | "devDependencies": { 14 | "vows": "0.7.x" 15 | }, 16 | "keywords": [ 17 | "topological", 18 | "sort", 19 | "sorting", 20 | "graphs", 21 | "graph", 22 | "dependency", 23 | "list", 24 | "dependencies", 25 | "acyclic" 26 | ], 27 | "author": "Marcel Klehr ", 28 | "license": "MIT" 29 | } 30 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , toposort = require('./index') 3 | , assert = require('assert') 4 | 5 | var suite = vows.describe('toposort') 6 | suite.addBatch( 7 | { 'acyclic graphs': 8 | { topic: function() { 9 | /*(read downwards) 10 | 6 3 11 | | | 12 | 5->2 13 | | | 14 | 4 1 15 | */ 16 | return toposort( 17 | [ ["3", '2'] 18 | , ["2", "1"] 19 | , ["6", "5"] 20 | , ["5", "2"] 21 | , ["5", "4"] 22 | ]) 23 | } 24 | , 'should be sorted correctly': function(er, result) { 25 | assert.instanceOf(result, Array) 26 | var failed = [], passed 27 | // valid permutations 28 | ;[ [ '3','6','5','2','1','4' ] 29 | , [ '3','6','5','2','4','1' ] 30 | , [ '6','3','5','2','1','4' ] 31 | , [ '6','5','3','2','1','4' ] 32 | , [ '6','5','3','2','4','1' ] 33 | , [ '6','5', '4','3','2','1' ] 34 | ].forEach(function(solution) { 35 | try { 36 | assert.deepEqual(result, solution) 37 | passed = true 38 | }catch (e) { 39 | failed.push(e) 40 | } 41 | }) 42 | if (!passed) { 43 | console.log(failed) 44 | throw failed[0]; 45 | } 46 | } 47 | } 48 | , 'simple cyclic graphs': 49 | { topic: function() { 50 | /* 51 | foo<->bar 52 | */ 53 | return toposort( 54 | [ ["foo", 'bar'] 55 | , ["bar", "foo"]// cyclic dependecy 56 | ]) 57 | } 58 | , 'should throw an exception': function(_, val) { 59 | assert.instanceOf(val, Error) 60 | } 61 | } 62 | , 'complex cyclic graphs': 63 | { topic: function() { 64 | /* 65 | foo 66 | | 67 | bar<-john 68 | | ^ 69 | ron->tom 70 | */ 71 | return toposort( 72 | [ ["foo", 'bar'] 73 | , ["bar", "ron"] 74 | , ["john", "bar"] 75 | , ["tom", "john"] 76 | , ["ron", "tom"]// cyclic dependecy 77 | ]) 78 | } 79 | , 'should throw an exception': function(_, val) { 80 | assert.instanceOf(val, Error) 81 | } 82 | } 83 | , 'unknown nodes in edges': 84 | { topic: function() { 85 | return toposort.array(['bla'], 86 | [ ["foo", 'bar'] 87 | , ["bar", "ron"] 88 | , ["john", "bar"] 89 | , ["tom", "john"] 90 | , ["ron", "tom"] 91 | ]) 92 | } 93 | , 'should throw an exception': function(_, val) { 94 | assert.instanceOf(val, Error) 95 | } 96 | } 97 | , 'triangular dependency': 98 | { topic: function() { 99 | /* 100 | a-> b 101 | | / 102 | c<- 103 | */ 104 | return toposort([ 105 | ['a', 'b'] 106 | , ['a', 'c'] 107 | , ['b', 'c'] 108 | ]); 109 | } 110 | , 'shouldn\'t throw an error': function(er, result) { 111 | assert.deepEqual(result, ['a', 'b', 'c']) 112 | } 113 | } 114 | , 'toposort.array': 115 | { topic: function() { 116 | return toposort.array(['d', 'c', 'a', 'b'], [['a','b'],['b','c']]) 117 | } 118 | , 'should include unconnected nodes': function(er, result){ 119 | var i = result.indexOf('d') 120 | assert(i >= 0) 121 | result.splice(i, 1) 122 | assert.deepEqual(result, ['a', 'b', 'c']) 123 | } 124 | } 125 | , 'toposort.array mutation': 126 | { topic: function() { 127 | var array = ['d', 'c', 'a', 'b'] 128 | toposort.array(array, [['a','b'],['b','c']]) 129 | return array 130 | } 131 | , 'should not mutate its arguments': function(er, result){ 132 | assert.deepEqual(result, ['d', 'c', 'a', 'b']) 133 | } 134 | } 135 | , 'giant graphs': 136 | { 137 | topic: function() { 138 | var graph = [] 139 | , nodeCount = 100000 140 | for (var i = 0; i < nodeCount; i++) { 141 | graph.push([i, i + 1]) 142 | } 143 | return graph 144 | } 145 | , 'should sort quickly': function(er, result){ 146 | var start = (new Date).getTime() 147 | var sorted = toposort(result) 148 | var end = (new Date).getTime() 149 | var elapsedSeconds = (end - start) / 1000 150 | assert(elapsedSeconds < 1) 151 | } 152 | } 153 | , 'object keys': 154 | { 155 | topic: function() { 156 | var o1 = {k1: 'v1', nested: {k2: 'v2'}} 157 | var o2 = {k2: 'v2'} 158 | var o3 = {k3: 'v3'} 159 | var graph = [[o1, o2], [o2, o3]] 160 | return graph 161 | } 162 | , 'should handle object nodes': function(er, result){ 163 | var sorted = toposort(result) 164 | assert.deepEqual(sorted, [{k1: 'v1', nested: {k2: 'v2'}}, {k2: 'v2'}, {k3: 'v3'}]) 165 | } 166 | } 167 | }) 168 | .run(null, function() { 169 | (suite.results.broken+suite.results.errored) > 0 && process.exit(1) 170 | process.exit(0) 171 | }) 172 | --------------------------------------------------------------------------------