dagjs
Version:
Simiple DAG (Directed Acyclic Graph) module with edge tagging
488 lines (432 loc) • 13.2 kB
JavaScript
const CycleError = require('./cycle-error');
class Dag {
constructor() {
this.edges = [];
this.tagObjs = {};
this.tagInvertedIndex = {};
}
get T() {
return Object.keys(this.tagObjs)
.reduce((previous, tag) => previous.concat([tag]), []);
}
get E() {
const edges = [];
Object.keys(this.edges).forEach((to) => {
this.edges[to].forEach(edge => edges.push(this.edge(edge.f, to)));
});
return edges;
}
get V() {
const verticies = Object.keys(this.edges).reduce((previous, key) => {
if (this.edges[key].length > 0) {
if (!previous.includes(key)) {
previous.push(key);
}
this.edges[key].forEach((e) => {
if (!previous.includes(e.f)) {
previous.push(e.f);
}
});
}
return previous;
}, []);
return verticies;
}
get kind() {
const tags = this.T;
return tags.length;
}
get order() {
const verticies = this.V;
return verticies.length;
}
get size() {
return Object.keys(this.edges).reduce((previous, key) => previous + this.edges[key].length, 0);
}
/*
recursiveStringify(obj, depth=0) {
if (obj == undefined)
return;
var objKeys = Object.keys(obj);
var keyValueArray = new Array();
for (var i = 0; i < objKeys.length; i++) {
var keyValueString = '"' + objKeys[i] + '":';
var objValue = obj[objKeys[i]];
keyValueString = (typeof objValue == "string") ?
keyValueString = keyValueString + '"' + objValue + '"' :
keyValueString = keyValueString + this.recursiveStringify(objValue, depth+1);
keyValueArray.push(keyValueString);
}
let indent = "";
const tab = " ";
for(i=0; i<depth; i++)
indent += tab;
if (Array.isArray(obj))
return tab + "[\n" +
indent+ tab + keyValueArray.join("," + "\n" + indent + tab) + "\n" +
indent + "]";
else
return tab + "{\n" +
indent+ tab + keyValueArray.join("," + "\n" + indent + tab) + "\n" +
indent + "}";
}
get json() {
return this.recursiveStringify(this);
}
debug() {
console.log(this.json);
}
*/
edge(from, to) {
if (this.edges[to] !== undefined) {
const edge = this.edges[to].find(e => e.f === from);
if (edge !== undefined) {
return {
from: edge.f,
to,
weight: edge.w,
tags: edge.ts.reduce((previous, tagObject) => previous.concat([tagObject.name]), []),
};
}
}
return undefined;
}
includes(from, to) {
return this.edge(from, to) !== undefined;
}
add(from, to, tags, weight) {
// test cycle
if (this.testCycle(from, to)) {
throw new CycleError();
}
// instantiate tags, if not exist
let tagArray = tags;
if (!Array.isArray(tags)) {
tagArray = [tags];
}
tagArray.forEach((tag) => {
if (!(tag in this.tagObjs)) {
this.tagObjs[tag] = { name: tag };
}
});
// instantiate an edge
const edge = {
f: from,
w: weight,
ts: tagArray.reduce((previous, tag) => previous.concat([this.tagObjs[tag]]), []),
};
// register the edge
if (this.edges[to] === undefined) {
this.edges[to] = [];
}
this.edges[to].push(edge);
tagArray.forEach((tag) => {
if (!(tag in this.tagInvertedIndex)) {
this.tagInvertedIndex[tag] = {};
}
if (!(to in this.tagInvertedIndex[tag])) {
this.tagInvertedIndex[tag][to] = [];
}
this.tagInvertedIndex[tag][to].push(edge);
});
return this;
}
/**
* @param {string} start - starting point of reverse-BFS
* @callback hitCondition - condition to stop traversal
* @callback callback - task to do for each visit. The visit stopping the traversal is exclusive.
*/
reverseBFS(start, hitCondition, callback) {
const q = [start];
while (q.length > 0) {
const visit = q.shift();
if (hitCondition !== undefined && hitCondition(visit)) {
return visit;
}
if (callback !== undefined) {
callback(visit);
}
if (this.edges[visit] !== undefined) {
this.edges[visit].forEach(e => q.push(e.f));
}
}
return undefined;
}
/**
* @return {boolean} true, if (@code{from}, @code{to}) makes a cycle. false, otherwise.
*/
testCycle(from, to) {
if (from === to) {
return true;
}
const hit = this.reverseBFS(from, v => v === to);
if (hit === undefined) {
return false;
}
return true;
}
// clone
/**
* Shallow copy
* @return new DAG instance which has new arrays (i.e., E, tagObjs, and tagInvertedIndex) but
* their elements (i.e., E['a'], ... and tagObjs['friend'], ...).
*/
clone() {
const newDag = new Dag();
Object.keys(this.edges).forEach((key) => { newDag.edges[key] = this.edges[key]; });
Object.keys(this.tagObjs).forEach((tag) => { newDag.tagObjs[tag] = this.tagObjs[tag]; });
Object.keys(this.tagInvertedIndex)
.forEach((tag) => { newDag.tagInvertedIndex[tag] = this.tagInvertedIndex[tag]; });
return newDag;
}
/**
* Deep copy
* @return new DAG instance without references from the original pieces, at all.
*/
deepClone() {
const newDag = new Dag();
Object.keys(this.edges).forEach((to) => {
this.edges[to].forEach((e) => {
// clone the edge
const tags = e.ts.reduce((previous, tagObj) => previous.concat([tagObj.name]), []);
newDag.add(e.f, to, tags, e.w);
});
});
return newDag;
}
// neighbouring
/**
* Edges comes from a vertex
* @param {string} from the vertex.
* @returns {@type {Dag}} deep cloned edges start at the vertex 'from'.
* empty DAG, if the vertex does not exist or there is no edges from it.
*/
edgesFrom(from) {
const dag = new Dag();
Object.keys(this.edges).forEach((key) => {
this.edges[key].forEach((e) => {
if (e.f === from) {
const cloned = { from: e.f, to: key, weight: e.w };
cloned.tags = e.ts.reduce((previous, tagObject) => previous.concat([tagObject.name]), []);
dag.add(cloned.from, cloned.to, cloned.tags, cloned.weight);
}
});
});
return dag;
}
/**
* Edges go to a vertex
* @param {string} to the vertex.
* @returns {@type {Dag}} deep cloned edges end at the vertex 'to'.
* empty DAG, if the vertex does not exist or there is no edges heading
* it.
*/
edgesTo(to) {
if (undefined === this.edges[to]) {
return new Dag();
}
const dag = new Dag();
this.edges[to].forEach((e) => {
const cloned = { from: e.f, to, weight: e.w };
cloned.tags = e.ts.reduce((previous, tagObject) => previous.concat([tagObject.name]), []);
dag.add(cloned.from, cloned.to, cloned.tags, cloned.weight);
});
return dag;
}
/**
* sub-DAG whoes edges are around a vertex
* @param {string} vertex the vertex.
* @returns {Object} deep cloned DAG where all the edges relate with the vertex.
* empty DAG, if the vertex does not exist or there is no edges around
* it.
*/
neighbourhood(vertex) {
const dag = this.edgesFrom(vertex);
if (undefined === this.edges[vertex]) {
return dag;
}
this.edges[vertex].forEach((e) => {
const cloned = { from: e.f, to: vertex, weight: e.w };
cloned.tags = e.ts.reduce((previous, tagObject) => previous.concat([tagObject.name]), []);
dag.add(cloned.from, cloned.to, cloned.tags, cloned.weight);
});
return dag;
}
// tag
/**
* Number of verticies with a tag
* It counts the number of verticies connected by edges of the given tag.
* In counting, it refers to this.tagInvertedIndex.
* @param {string} tag the tag.
* @returns {number} the number of verticies by 'tag'.
*/
tagOrder(tag) {
if (!(tag in this.tagInvertedIndex)) {
return 0;
}
const verticies = Object.keys(this.tagInvertedIndex[tag]).reduce((previous, to) => {
if (this.tagInvertedIndex[tag][to].length > 0) {
if (!previous.includes(to)) {
previous.push(to);
}
this.tagInvertedIndex[tag][to].forEach((e) => {
if (!previous.includes(e.f)) {
previous.push(e.f);
}
});
}
return previous;
}, []);
return verticies.length;
}
/**
* Number of edges with a tag
* It counts the number of edges filtered by the given tag.
* In counting, it refers to this.tagInvertedIndex.
* @param {string} tag the tag.
* @returns {number} the number of edges by 'tag'.
*/
tagSize(tag) {
if (!(tag in this.tagInvertedIndex)) {
return 0;
}
return Object.keys(this.tagInvertedIndex[tag])
.reduce((count, to) => count + this.tagInvertedIndex[tag][to].length, 0);
}
/**
* Number of tags in edges with a tag
* It counts the number of tags attached to the edges filtered by the given tag.
* In counting, it refers to this.tagInvertedIndex.
* @param {string} tag the tag, which is inclusive to the count.
* @returns {number} the number of tags in edges by 'tag'.
*/
tagKind(tag) {
if (!(tag in this.tagInvertedIndex)) {
return 0;
}
const tags = Object.keys(this.tagInvertedIndex[tag]).reduce((previous, to) => {
if (this.tagInvertedIndex[tag][to].length > 0) {
this.tagInvertedIndex[tag][to].forEach((e) => {
e.ts.forEach((tagObj) => {
if (!previous.includes(tagObj.name)) {
previous.push(tagObj.name);
}
});
});
}
return previous;
}, [tag]);
return tags.length;
}
/**
* Break DAG by a tag
* @param {string} tag A tag to split this DAG by
* @returns {array} A deep-cloned sub-DAG of itself, if there is an edge including 'tag'.
* Undefined, otherwise.
*/
filterByTag(tag) {
if (!(tag in this.tagInvertedIndex)) {
return undefined;
}
const filteredEdges = this.tagInvertedIndex[tag];
const filtered = new Dag();
Object.keys(filteredEdges).forEach((to) => {
filteredEdges[to].forEach((edge) => {
const cloned = { from: edge.f, to, tags: [], weight: undefined };
cloned.tags = edge.ts.reduce((previous, tagObj) => previous.concat([tagObj.name]), []);
filtered.add(cloned.from, cloned.to, cloned.tags, cloned.weight);
});
});
return filtered;
}
// remove
removeEdge(from, to) {
if (!(to in this.edges)) {
return this;
}
// arrange edges
const targetIndex = this.edges[to].findIndex(e => e.f === from);
if (targetIndex === -1) {
return this;
}
const removed = this.edges[to].splice(targetIndex, 1)[0];
if (this.edges[to].length === 0) {
delete this.edges[to];
}
// arrange tag index
removed.ts.forEach((tagObj) => {
this.tagInvertedIndex[tagObj.name][to] =
this.tagInvertedIndex[tagObj.name][to].filter(edge => edge.f !== from);
if (this.tagInvertedIndex[tagObj.name][to].length === 0) {
delete this.tagInvertedIndex[tagObj.name][to];
}
});
// remove dangling tags
removed.ts.forEach((tagObj) => {
if (this.tagSize(tagObj.name) === 0) {
delete this.tagObjs[tagObj.name];
delete this.tagInvertedIndex[tagObj.name];
}
});
return this;
}
removeVertex(vertex) {
// remove edges 'to' the verted
if (vertex in this.edges) {
// arrange edges
delete this.edges[vertex];
// arrange tag index
Object.keys(this.tagInvertedIndex).forEach((tag) => {
delete this.tagInvertedIndex[tag][vertex];
});
}
// remove edges 'from' the vertex
Object.keys(this.edges).forEach((to) => {
// arrnage edges
const tagsToRemove = [];
this.edges[to] = this.edges[to].filter((e) => {
e.ts.forEach((tagObj) => {
if (!tagsToRemove.includes(tagObj.name)) {
tagsToRemove.push(tagObj.name);
}
});
return e.f !== vertex;
});
if (this.edges[to].length === 0) {
delete this.edges[to];
}
// arrange tag index
tagsToRemove.forEach((tag) => {
this.tagInvertedIndex[tag][to] = this.tagInvertedIndex[tag][to].filter(e => e.f !== vertex);
if (this.tagInvertedIndex[tag][to].length === 0) {
delete this.tagInvertedIndex[tag][to];
}
});
});
// remove dangling tags
Object.keys(this.tagObjs).forEach((tag) => {
if (this.tagSize(tag) === 0) {
delete this.tagObjs[tag];
delete this.tagInvertedIndex[tag];
}
});
return this;
}
/*
// sorting
topologicalSort() {
return [];
}
// sub-DAG
since(from) {return this;}
until(to) {return this;}
// line graph
lineDag() {return this;}
// edge
contractEdge(from, to) {return this;}
// ged
//ged(dag, distAlg) {}
// serialize & deserialize
*/
}
module.exports = Dag;