Spaces:
Build error
Build error
import * as _ from 'lodash-es'; | |
var DEFAULT_EDGE_NAME = '\x00'; | |
var GRAPH_NODE = '\x00'; | |
var EDGE_KEY_DELIM = '\x01'; | |
// Implementation notes: | |
// | |
// * Node id query functions should return string ids for the nodes | |
// * Edge id query functions should return an "edgeObj", edge object, that is | |
// composed of enough information to uniquely identify an edge: {v, w, name}. | |
// * Internally we use an "edgeId", a stringified form of the edgeObj, to | |
// reference edges. This is because we need a performant way to look these | |
// edges up and, object properties, which have string keys, are the closest | |
// we're going to get to a performant hashtable in JavaScript. | |
// Implementation notes: | |
// | |
// * Node id query functions should return string ids for the nodes | |
// * Edge id query functions should return an "edgeObj", edge object, that is | |
// composed of enough information to uniquely identify an edge: {v, w, name}. | |
// * Internally we use an "edgeId", a stringified form of the edgeObj, to | |
// reference edges. This is because we need a performant way to look these | |
// edges up and, object properties, which have string keys, are the closest | |
// we're going to get to a performant hashtable in JavaScript. | |
export class Graph { | |
constructor(opts = {}) { | |
this._isDirected = _.has(opts, 'directed') ? opts.directed : true; | |
this._isMultigraph = _.has(opts, 'multigraph') ? opts.multigraph : false; | |
this._isCompound = _.has(opts, 'compound') ? opts.compound : false; | |
// Label for the graph itself | |
this._label = undefined; | |
// Defaults to be set when creating a new node | |
this._defaultNodeLabelFn = _.constant(undefined); | |
// Defaults to be set when creating a new edge | |
this._defaultEdgeLabelFn = _.constant(undefined); | |
// v -> label | |
this._nodes = {}; | |
if (this._isCompound) { | |
// v -> parent | |
this._parent = {}; | |
// v -> children | |
this._children = {}; | |
this._children[GRAPH_NODE] = {}; | |
} | |
// v -> edgeObj | |
this._in = {}; | |
// u -> v -> Number | |
this._preds = {}; | |
// v -> edgeObj | |
this._out = {}; | |
// v -> w -> Number | |
this._sucs = {}; | |
// e -> edgeObj | |
this._edgeObjs = {}; | |
// e -> label | |
this._edgeLabels = {}; | |
} | |
/* === Graph functions ========= */ | |
isDirected() { | |
return this._isDirected; | |
} | |
isMultigraph() { | |
return this._isMultigraph; | |
} | |
isCompound() { | |
return this._isCompound; | |
} | |
setGraph(label) { | |
this._label = label; | |
return this; | |
} | |
graph() { | |
return this._label; | |
} | |
/* === Node functions ========== */ | |
setDefaultNodeLabel(newDefault) { | |
if (!_.isFunction(newDefault)) { | |
newDefault = _.constant(newDefault); | |
} | |
this._defaultNodeLabelFn = newDefault; | |
return this; | |
} | |
nodeCount() { | |
return this._nodeCount; | |
} | |
nodes() { | |
return _.keys(this._nodes); | |
} | |
sources() { | |
var self = this; | |
return _.filter(this.nodes(), function (v) { | |
return _.isEmpty(self._in[v]); | |
}); | |
} | |
sinks() { | |
var self = this; | |
return _.filter(this.nodes(), function (v) { | |
return _.isEmpty(self._out[v]); | |
}); | |
} | |
setNodes(vs, value) { | |
var args = arguments; | |
var self = this; | |
_.each(vs, function (v) { | |
if (args.length > 1) { | |
self.setNode(v, value); | |
} else { | |
self.setNode(v); | |
} | |
}); | |
return this; | |
} | |
setNode(v, value) { | |
if (_.has(this._nodes, v)) { | |
if (arguments.length > 1) { | |
this._nodes[v] = value; | |
} | |
return this; | |
} | |
// @ts-expect-error | |
this._nodes[v] = arguments.length > 1 ? value : this._defaultNodeLabelFn(v); | |
if (this._isCompound) { | |
this._parent[v] = GRAPH_NODE; | |
this._children[v] = {}; | |
this._children[GRAPH_NODE][v] = true; | |
} | |
this._in[v] = {}; | |
this._preds[v] = {}; | |
this._out[v] = {}; | |
this._sucs[v] = {}; | |
++this._nodeCount; | |
return this; | |
} | |
node(v) { | |
return this._nodes[v]; | |
} | |
hasNode(v) { | |
return _.has(this._nodes, v); | |
} | |
removeNode(v) { | |
var self = this; | |
if (_.has(this._nodes, v)) { | |
var removeEdge = function (e) { | |
self.removeEdge(self._edgeObjs[e]); | |
}; | |
delete this._nodes[v]; | |
if (this._isCompound) { | |
this._removeFromParentsChildList(v); | |
delete this._parent[v]; | |
_.each(this.children(v), function (child) { | |
self.setParent(child); | |
}); | |
delete this._children[v]; | |
} | |
_.each(_.keys(this._in[v]), removeEdge); | |
delete this._in[v]; | |
delete this._preds[v]; | |
_.each(_.keys(this._out[v]), removeEdge); | |
delete this._out[v]; | |
delete this._sucs[v]; | |
--this._nodeCount; | |
} | |
return this; | |
} | |
setParent(v, parent) { | |
if (!this._isCompound) { | |
throw new Error('Cannot set parent in a non-compound graph'); | |
} | |
if (_.isUndefined(parent)) { | |
parent = GRAPH_NODE; | |
} else { | |
// Coerce parent to string | |
parent += ''; | |
for (var ancestor = parent; !_.isUndefined(ancestor); ancestor = this.parent(ancestor)) { | |
if (ancestor === v) { | |
throw new Error('Setting ' + parent + ' as parent of ' + v + ' would create a cycle'); | |
} | |
} | |
this.setNode(parent); | |
} | |
this.setNode(v); | |
this._removeFromParentsChildList(v); | |
this._parent[v] = parent; | |
this._children[parent][v] = true; | |
return this; | |
} | |
_removeFromParentsChildList(v) { | |
delete this._children[this._parent[v]][v]; | |
} | |
parent(v) { | |
if (this._isCompound) { | |
var parent = this._parent[v]; | |
if (parent !== GRAPH_NODE) { | |
return parent; | |
} | |
} | |
} | |
children(v) { | |
if (_.isUndefined(v)) { | |
v = GRAPH_NODE; | |
} | |
if (this._isCompound) { | |
var children = this._children[v]; | |
if (children) { | |
return _.keys(children); | |
} | |
} else if (v === GRAPH_NODE) { | |
return this.nodes(); | |
} else if (this.hasNode(v)) { | |
return []; | |
} | |
} | |
predecessors(v) { | |
var predsV = this._preds[v]; | |
if (predsV) { | |
return _.keys(predsV); | |
} | |
} | |
successors(v) { | |
var sucsV = this._sucs[v]; | |
if (sucsV) { | |
return _.keys(sucsV); | |
} | |
} | |
neighbors(v) { | |
var preds = this.predecessors(v); | |
if (preds) { | |
return _.union(preds, this.successors(v)); | |
} | |
} | |
isLeaf(v) { | |
var neighbors; | |
if (this.isDirected()) { | |
neighbors = this.successors(v); | |
} else { | |
neighbors = this.neighbors(v); | |
} | |
return neighbors.length === 0; | |
} | |
filterNodes(filter) { | |
// @ts-expect-error | |
var copy = new this.constructor({ | |
directed: this._isDirected, | |
multigraph: this._isMultigraph, | |
compound: this._isCompound, | |
}); | |
copy.setGraph(this.graph()); | |
var self = this; | |
_.each(this._nodes, function (value, v) { | |
if (filter(v)) { | |
copy.setNode(v, value); | |
} | |
}); | |
_.each(this._edgeObjs, function (e) { | |
// @ts-expect-error | |
if (copy.hasNode(e.v) && copy.hasNode(e.w)) { | |
copy.setEdge(e, self.edge(e)); | |
} | |
}); | |
var parents = {}; | |
function findParent(v) { | |
var parent = self.parent(v); | |
if (parent === undefined || copy.hasNode(parent)) { | |
parents[v] = parent; | |
return parent; | |
} else if (parent in parents) { | |
return parents[parent]; | |
} else { | |
return findParent(parent); | |
} | |
} | |
if (this._isCompound) { | |
_.each(copy.nodes(), function (v) { | |
copy.setParent(v, findParent(v)); | |
}); | |
} | |
return copy; | |
} | |
/* === Edge functions ========== */ | |
setDefaultEdgeLabel(newDefault) { | |
if (!_.isFunction(newDefault)) { | |
newDefault = _.constant(newDefault); | |
} | |
this._defaultEdgeLabelFn = newDefault; | |
return this; | |
} | |
edgeCount() { | |
return this._edgeCount; | |
} | |
edges() { | |
return _.values(this._edgeObjs); | |
} | |
setPath(vs, value) { | |
var self = this; | |
var args = arguments; | |
_.reduce(vs, function (v, w) { | |
if (args.length > 1) { | |
self.setEdge(v, w, value); | |
} else { | |
self.setEdge(v, w); | |
} | |
return w; | |
}); | |
return this; | |
} | |
/* | |
* setEdge(v, w, [value, [name]]) | |
* setEdge({ v, w, [name] }, [value]) | |
*/ | |
setEdge() { | |
var v, w, name, value; | |
var valueSpecified = false; | |
var arg0 = arguments[0]; | |
if (typeof arg0 === 'object' && arg0 !== null && 'v' in arg0) { | |
v = arg0.v; | |
w = arg0.w; | |
name = arg0.name; | |
if (arguments.length === 2) { | |
value = arguments[1]; | |
valueSpecified = true; | |
} | |
} else { | |
v = arg0; | |
w = arguments[1]; | |
name = arguments[3]; | |
if (arguments.length > 2) { | |
value = arguments[2]; | |
valueSpecified = true; | |
} | |
} | |
v = '' + v; | |
w = '' + w; | |
if (!_.isUndefined(name)) { | |
name = '' + name; | |
} | |
var e = edgeArgsToId(this._isDirected, v, w, name); | |
if (_.has(this._edgeLabels, e)) { | |
if (valueSpecified) { | |
this._edgeLabels[e] = value; | |
} | |
return this; | |
} | |
if (!_.isUndefined(name) && !this._isMultigraph) { | |
throw new Error('Cannot set a named edge when isMultigraph = false'); | |
} | |
// It didn't exist, so we need to create it. | |
// First ensure the nodes exist. | |
this.setNode(v); | |
this.setNode(w); | |
// @ts-expect-error | |
this._edgeLabels[e] = valueSpecified ? value : this._defaultEdgeLabelFn(v, w, name); | |
var edgeObj = edgeArgsToObj(this._isDirected, v, w, name); | |
// Ensure we add undirected edges in a consistent way. | |
v = edgeObj.v; | |
w = edgeObj.w; | |
Object.freeze(edgeObj); | |
this._edgeObjs[e] = edgeObj; | |
incrementOrInitEntry(this._preds[w], v); | |
incrementOrInitEntry(this._sucs[v], w); | |
this._in[w][e] = edgeObj; | |
this._out[v][e] = edgeObj; | |
this._edgeCount++; | |
return this; | |
} | |
edge(v, w, name) { | |
var e = | |
arguments.length === 1 | |
? edgeObjToId(this._isDirected, arguments[0]) | |
: edgeArgsToId(this._isDirected, v, w, name); | |
return this._edgeLabels[e]; | |
} | |
hasEdge(v, w, name) { | |
var e = | |
arguments.length === 1 | |
? edgeObjToId(this._isDirected, arguments[0]) | |
: edgeArgsToId(this._isDirected, v, w, name); | |
return _.has(this._edgeLabels, e); | |
} | |
removeEdge(v, w, name) { | |
var e = | |
arguments.length === 1 | |
? edgeObjToId(this._isDirected, arguments[0]) | |
: edgeArgsToId(this._isDirected, v, w, name); | |
var edge = this._edgeObjs[e]; | |
if (edge) { | |
v = edge.v; | |
w = edge.w; | |
delete this._edgeLabels[e]; | |
delete this._edgeObjs[e]; | |
decrementOrRemoveEntry(this._preds[w], v); | |
decrementOrRemoveEntry(this._sucs[v], w); | |
delete this._in[w][e]; | |
delete this._out[v][e]; | |
this._edgeCount--; | |
} | |
return this; | |
} | |
inEdges(v, u) { | |
var inV = this._in[v]; | |
if (inV) { | |
var edges = _.values(inV); | |
if (!u) { | |
return edges; | |
} | |
return _.filter(edges, function (edge) { | |
return edge.v === u; | |
}); | |
} | |
} | |
outEdges(v, w) { | |
var outV = this._out[v]; | |
if (outV) { | |
var edges = _.values(outV); | |
if (!w) { | |
return edges; | |
} | |
return _.filter(edges, function (edge) { | |
return edge.w === w; | |
}); | |
} | |
} | |
nodeEdges(v, w) { | |
var inEdges = this.inEdges(v, w); | |
if (inEdges) { | |
return inEdges.concat(this.outEdges(v, w)); | |
} | |
} | |
} | |
/* Number of nodes in the graph. Should only be changed by the implementation. */ | |
Graph.prototype._nodeCount = 0; | |
/* Number of edges in the graph. Should only be changed by the implementation. */ | |
Graph.prototype._edgeCount = 0; | |
function incrementOrInitEntry(map, k) { | |
if (map[k]) { | |
map[k]++; | |
} else { | |
map[k] = 1; | |
} | |
} | |
function decrementOrRemoveEntry(map, k) { | |
if (!--map[k]) { | |
delete map[k]; | |
} | |
} | |
function edgeArgsToId(isDirected, v_, w_, name) { | |
var v = '' + v_; | |
var w = '' + w_; | |
if (!isDirected && v > w) { | |
var tmp = v; | |
v = w; | |
w = tmp; | |
} | |
return v + EDGE_KEY_DELIM + w + EDGE_KEY_DELIM + (_.isUndefined(name) ? DEFAULT_EDGE_NAME : name); | |
} | |
function edgeArgsToObj(isDirected, v_, w_, name) { | |
var v = '' + v_; | |
var w = '' + w_; | |
if (!isDirected && v > w) { | |
var tmp = v; | |
v = w; | |
w = tmp; | |
} | |
var edgeObj = { v: v, w: w }; | |
if (name) { | |
edgeObj.name = name; | |
} | |
return edgeObj; | |
} | |
function edgeObjToId(isDirected, edgeObj) { | |
return edgeArgsToId(isDirected, edgeObj.v, edgeObj.w, edgeObj.name); | |
} | |