topological-sort-group
Version:
Topological sorting and cycle detection. Optional grouping for parallel processing
129 lines (128 loc) • 4.59 kB
JavaScript
import get from './deepGet.js';
import { SortMode } from './types.js';
const isArray = Array.isArray || ((x)=>Object.prototype.toString.call(x) === '[object Array]');
let Graph = class Graph {
static from(values, options) {
const graph = new Graph(options);
values.forEach((value)=>isArray(value) ? graph.add(value[0], value[1]) : graph.add(value));
return graph;
}
key(keyOrValue) {
if (this.path) return typeof keyOrValue === 'object' ? get(keyOrValue, this.path) : keyOrValue;
return keyOrValue;
}
keys() {
const keys = [];
for(const key in this.nodeMap)keys.push(key);
return keys;
}
value(key) {
return this.nodeMap[key].value;
}
edges(key) {
return this.nodeMap[key].edges;
}
add(keyOrValue, toKeyOrValue) {
const key = this.key(keyOrValue);
const value = this.path ? typeof keyOrValue === 'object' ? keyOrValue : undefined : keyOrValue;
if (value !== undefined) {
if (this.nodeMap[key] === undefined) {
this.nodeMap[key] = {
value: value,
edges: []
};
this.size++;
} else if (this.nodeMap[key].value !== value) throw new Error(`Adding different node values to same graph. Key ${key}. Existing: ${JSON.stringify(this.nodeMap[key].value)}. New: ${JSON.stringify(value)}`);
}
// biome-ignore lint/complexity/noArguments: Apply arguments
if (arguments.length === 1) return;
// add edge
this.add(toKeyOrValue);
const toKey = this.key(toKeyOrValue);
this.nodeMap[key].edges.push(toKey);
}
degrees() {
const degrees = {};
for(const from in this.nodeMap){
if (degrees[from] === undefined) degrees[from] = 0;
this.nodeMap[from].edges.forEach((key)=>{
if (degrees[key] === undefined) degrees[key] = 0;
degrees[key]++;
});
}
return degrees;
}
cycles() {
const visited = {};
const marks = {};
const cycles = [];
const visit = (key, ancestors)=>{
if (marks[key]) return cycles.push(ancestors.concat(key)); // found a cycle
if (visited[key]) return; // already visited
visited[key] = true;
// check for cycles from this key
marks[key] = true;
this.edges(key).forEach((neighborKey)=>{
visit(neighborKey, ancestors.concat(key));
});
delete marks[key];
};
// check all keys
let keys = this.keys();
while(keys.length > 0){
visit(keys[0], []);
keys = keys.filter((key)=>!visited[key]); // remove processed
}
return cycles;
}
sort(mode = SortMode.Group) {
const degrees = this.degrees();
// Initialize queue with no links
const queue = [];
for(const key in degrees){
if (degrees[key] === 0) queue.push({
key,
level: 0
});
}
const nodes = [];
const groups = [];
// process each level
let processed = 0;
let level = 0;
while(queue.length > 0){
const queued = queue.shift();
// If we're moving to a new level, store the previous level's nodes
if (mode === SortMode.Group && queued.level > level) {
groups.push(nodes.splice(0, nodes.length));
level = queued.level;
}
nodes.push(this.value(queued.key));
processed++;
// Process neighbors
const neighbors = this.edges(queued.key);
for(let i = 0; i < neighbors.length; i++){
const key = neighbors[i];
if (--degrees[key] === 0) {
queue.push({
key,
level: level + 1
});
}
}
}
// Add the last nodes
if (mode === SortMode.Group && nodes.length > 0) groups.push(nodes);
return {
nodes: mode === SortMode.Group ? groups : nodes,
cycles: processed !== this.size ? this.cycles() : []
};
}
constructor(options){
this.size = 0;
this.path = options === null || options === void 0 ? void 0 : options.path;
this.nodeMap = {};
}
};
Graph.SortMode = SortMode;
export { Graph as default };