UNPKG

topological-sort-group

Version:

Topological sorting and cycle detection. Optional grouping for parallel processing

129 lines (128 loc) 4.59 kB
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 };