dag.coffee | |
---|---|
dag is a simple library for working with directed acyclic graphs. Its primary purpose is to support command execution pipelines for icing. dag's key operations are:
Our graph is composed of Nodes and Arcs. Each node has a unique name. Each arc flows out of one node and into another. A source is a node with no incoming edges. A topological ordering is a "linear ordering of its nodes in which each node comes before all nodes which it has outbound edges." Disclaimer: dag's intention is not to be a complete or robust DAG library. Nor is it meant to be highly performant, just simple. | |
Dependencies | _ = require 'underscore' |
Graph | class Graph |
A graph is just a collection of Nodes and Arcs. We keep maps of node names and arcs to ensure each node or arc between two nodes is represented by a single, unique object in the graph. | constructor: (@nodes = new NodeList, @arcs = new ArcList) ->
@nodeMap = {}
@arcMap = {}
graph = this
@nodes.forEach (node) -> graph.node node |
Add a node to the graph and give the node a reference back to the graph. Nodes are required to have unique names. If the same node is added twice we silently noop. Different nodes of the same name with throw an error. | node: (node) -> # Graph || Node
if _(node).isString()
if @nodeMap[node]?
return @nodeMap[node]
else
return undefined
if @nodeMap[node.name]?
if node.equals(@nodeMap[node.name])
return this
else
throw new Error "Node of name #{node.name} already exists."
@nodes.push node
@nodeMap[node.name] = node
this |
Add an arc to the graph by specifying the names of nodes from -arc-> to Ensure the arc is unique to the graph. | arc: (fromName, toName) -> # Graph
if @arcMap[fromName]?
if @arcMap[fromName][toName]?
return this
if not @nodeMap[fromName]?
throw new Error "Node #{fromName} does not exist"
else
fromNode = @nodeMap[fromName]
if not @nodeMap[toName]?
throw new Error "Node #{toName} does not exist"
else
toNode = @nodeMap[toName] |
Create the Arc | arc = new Arc fromNode, toNode
@arcs.push arc |
Cache it in the arcMap | if not @arcMap[fromNode.name]?
@arcMap[fromNode.name] = {}
@arcMap[fromNode.name][toNode.name] = arc
this |
Find sources by gathering all nodes that have inbound arcs and then removing those from the list of all nodes. | sources: -> # NodeList
nodesWithoutInboundArcs = do @nodes.clone
@arcs.to().forEach (node) ->
nodesWithoutInboundArcs.remove node
nodesWithoutInboundArcs |
Obtain a topological ordering by starting with source nodes. We visit each source node and:
If there are arcs left in the graph after this process there is a cycle in the graph. Otherwise we return the topologically sorted NodeList. | topologicalOrdering: -> # NodeList
nodes = new NodeList
sources = do this.sources
arcs = do @arcs.clone
while not sources.isEmpty()
source = do sources.pop
nodes.push source
arcs.from(source).forEach (arc) ->
arcs.remove arc
if arcs.to(arc.to).isEmpty()
sources.push arc.to
if arcs.isEmpty()
nodes
else
throw new Error "Cycle detected in graph." |
Utility function to determine if a cycle exists. Works by trying to find a topological ordering. If none exists then a cycle is present. | hasCycle: -> # bool
try
this.topologicalOrdering()
false
catch Error
true |
Obtain a subgraph by constructing a new Graph by backtracking from the target to sources. | subgraph: (target) -> #Graph
if this.hasCycle() then throw new Error "Can't create subgraph in a cyclic graph."
if not @nodeMap[target]? then throw new Error "Target #{target} does not exist."
else
target = @nodeMap[target]
graph = this
subgraph = new Graph
reconstruct = (to) ->
toClone = do to.clone
subgraph.node toClone
graph.arcs.to(to).forEach (arc) ->
fromClone = do arc.from.clone
subgraph.node fromClone
subgraph.arc fromClone.name, toClone.name
reconstruct arc.from
reconstruct target
subgraph |
Graph Components | |
The base Node only has a name. It retains a reference to its graph for looking up arcs inbound to and outbound from the Node. | class Node
constructor: (@name) ->
clone: -> return new Node @name
equals: (node) -> return @name == node.name |
Arc is a simple data structure referencing two unique nodes. | class Arc
constructor: (@from,@to) ->
if @from == @to then throw new Error "An arc's from Node cannot also be its to Node" |
List provides useful helpers around arrays that are used by Graph via subclasses NodeList and ArcList | class List
constructor: (@items = []) ->
push: (item) ->
if _(@items).contains item
@items = _(@items).without item
@items.push item
filter: (fn) -> _(@items).filter fn
clone: -> new List @items.slice 0
forEach: (fn) -> _(@items).forEach fn
remove: (item) -> @items = _(@items).without item
isEmpty: -> @items.length == 0
pop: -> do @items.pop
shift: -> do @items.shift
count: -> @items.length
pluck: (property) ->
_(@items).pluck property
class NodeList extends List
ofType: (type) -> new NodeList _(@items).filter (node) -> node instanceof type
clone: -> new NodeList super().items
names: -> _(@items).pluck 'name'
class ArcList extends List
from: (node) ->
if node?
new ArcList _(@items).filter (arc) -> arc.from == node
else
new NodeList this.pluckUniq 'from'
to: (node) ->
if node?
new ArcList _(@items).filter (arc) -> arc.to == node
else
new NodeList this.pluckUniq 'to'
pluckUniq: (property) ->
_(@items).chain().pluck(property).uniq().value()
clone: -> new ArcList super().items |
Exports | _(exports).extend {Node,Arc,List,NodeList,ArcList,Graph}
|