lively.lang_graph.js

/**
 * Computation over graphs. Unless otherwise specified a graph is a simple JS
 * object whose properties are interpreted as nodes that refer to arrays whose
 * elements describe edges.
 * 
 * ```js
 * var testGraph = {
 *   "a": ["b", "c"],
 *   "b": ["c", "d", "e", "f"],
 *   "d": ["c", "f"],
 *   "e": ["a", "f"],
 *   "f": []
 * }
 * ```
 * @module lively.lang/graph 
 */

import { range, shuffle, withoutAll } from './array.js';

/**
 * Returns a copy of graph map.
 * @param { Object.<string, string[]> } graph - The map to copy.
 * @returns { Object.<string, string[]> } The copy of the graph.
 */
function clone (graph) {
  const cloned = {};
  for (const id in graph) { cloned[id] = graph[id].slice(); }
  return cloned;
}

/**
 * Returns a copy of graph map with the given ids removed.
 * @param { Object.<string, string[]> } graph - The map to copy.
 * @param { string[] } ids - The list of ids to exclude in the copy.
 * @returns { Object.<string, string[]> } The (filtered) copy of the graph.
 */
function without (graph, ids) {
  const cloned = {};
  for (const id in graph) {
    if (ids.includes(id)) continue;
    cloned[id] = [];
    const refs = graph[id];
    for (let i = 0; i < refs.length; i++) {
      const ref = refs[i];
      if (!ids.includes(ref)) { cloned[id].push(ref); }
    }
  }
  return cloned;
}

/**
 * Takes a graph in object format and a start id and then traverses the
 * graph and gathers all nodes that can be reached from that start id.
 * Returns a list of those nodes.
 * Optionally use `ignore` list to filter out certain nodes that shouldn't
 * be considered and maxDepth to stop early. By default a maxDepth of 20 is
 * used.
 * @param { Object.<string, string[]> } g - The graph to traverse.
 * @param { string } id - The id of the node to start the hull computation from.
 * @param { string[] } ignoredKeyList - The ids of the nodes to skip from the traversal.
 * @param { number } [maxDepth] - The maximum recursion depth of the traversal.
 * @returns { string[] } The list of ids of all the nodes inside the hull.
 * @example
 * var testGraph = {
 * "a": ["b", "c"],
 * "b": ["c", "d", "e", "f"],
 * "d": ["c", "f"],
 * "e": ["a", "f"],
 * "f": []
 * }
 * hull(testGraph, "d") // => ["c", "f"]
 * hull(testGraph, "e") // => ['a', 'f', 'b', 'c', 'd', 'e']
 * hull(testGraph, "e", ["b"]) // => ["a", "f", "c"]
 */
function hull (g, id, ignoredKeyList = [], maxDepth = Infinity) {
  if (!Array.isArray(g[id])) return [];

  const hull = [];
  const visited = {};

  // Below is an optimized variant.
  // The functional but slow version:
  // return uniq(
  //         flatten(
  //           values(
  //             subgraphReachableBy(
  //               graphMap, id, ignore, maxDepth))));
  
  const ignoredKeys = {};
  for (let i = 0; i < ignoredKeyList.length; i++) { ignoredKeys[ignoredKeyList[i]] = true; }
  const toVisitList = g[id].slice();
  const toVisitMapAndDistFromRoot = {};
  for (let i = toVisitList.length; i--;) {
    const key = toVisitList[i];
    if (key in ignoredKeys) toVisitList.splice(i, 1);
    else toVisitMapAndDistFromRoot[key] = 1;
  }

  if (ignoredKeyList) {
    while (true) {
      if (toVisitList.length === 0) break;
      for (let i = 0; i < toVisitList.length; i++) {
        const key = toVisitList.shift();
        if (key in visited || key in ignoredKeys) continue;
        const dist = toVisitMapAndDistFromRoot[key] || 0;
        if (dist > maxDepth) continue;
        hull.push(key);
        visited[key] = true;
        const refs = g[key];
        if (!refs) continue;
        for (let j = 0; j < refs.length; j++) {
          const refKey = refs[j];
          if (refKey in visited || refKey in toVisitMapAndDistFromRoot) continue;
          toVisitMapAndDistFromRoot[refKey] = dist + 1;
          toVisitList.push(refKey);
        }
      }
    }
  }
  return hull;
}

/**
 * Like `hull` but returns subgraph map of `graphMap`.
 * @see hull
 * @param { Object.<string, string[]> } graphMap - The graph to compute the subgraph for.
 * @param { string } startId - The id of the node to start the subgraph computation from.
 * @param { string[] } ignore - The ids of the nodes to skip from the traversal.
 * @param { number } [maxDepth] - The maximum recursion depth of the traversal.
 * @returns { Object.<string, string[]> } The subgraph reachable from `id`.
 * @example
 * subgraphReachableBy(testGraph, "e", [], 2);
 * // => {e: [ 'a', 'f' ], a: [ 'b', 'c' ], f: []}
 */
function subgraphReachableBy (graphMap, startId, ignore, maxDepth = Infinity) {
  if (ignore) graphMap = without(graphMap, ignore);
  const ids = [startId]; let step = 0; const subgraph = {};
  while (ids.length && step++ < maxDepth) {
    const id = ids.shift();
    if (subgraph[id]) continue;
    const newIds = graphMap[id] || [];
    subgraph[id] = newIds;
    ids.push(...newIds);
  }
  return subgraph;
}

/**
 * Inverts the references of graph object `g`.
 * @param { Object.<string, string[]> } g - The graph to invert.
 * @returns { Object.<string, string[]> } The inverted graph.
 * @example
 * invert({a: ["b"], b: ["a", "c"]})
 *   // => {a: ["b"], b: ["a"], c: ["b"]}
 */
function invert (g) {
  const inverted = {};
  for (const key in g) {
    const refs = g[key];
    for (let i = 0; i < refs.length; i++) {
      const key2 = refs[i];
      if (!inverted[key2]) inverted[key2] = [key];
      else inverted[key2].push(key);
    }
  }
  return inverted;
}

/**
 * Sorts graph into an array of arrays. Each "bucket" contains the graph
 * nodes that have no other incoming nodes than those already visited. This
 * means, we start with the leaf nodes and then walk our way up.
 * This is useful for computing how to traverse a dependency graph: You get
 * a sorted list of dependencies that also allows circular references.
 * @param { Object.<string, string[]> } depGraph - The graph to sort into buckets.
 * @param { string } startNode - The id of the node at the top of the dependency graph.
 * @returns { string[][] } The sorted sets of nodes.
 * @example
 * var depGraph = {a: ["b", "c"], b: ["c"], c: ["b"]};
 * sortByReference(depGraph, "a");
 * // => [["c"], ["b"], ["a"]]
 */
function sortByReference (depGraph, startNode) {
  // establish unique list of keys
  const remaining = []; const remainingSeen = {}; const uniqDepGraph = {}; const inverseDepGraph = {};
  for (const key in depGraph) {
    if (!remainingSeen.hasOwnProperty(key)) {
      remainingSeen[key] = true;
      remaining.push(key);
    }
    const deps = depGraph[key]; const uniqDeps = {};
    if (deps) {
      uniqDepGraph[key] = [];
      for (const dep of deps) {
        if (uniqDeps.hasOwnProperty(dep) || key === dep) continue;
        const inverse = inverseDepGraph[dep] || (inverseDepGraph[dep] = []);
        if (!inverse.includes(key)) inverse.push(key);
        uniqDeps[dep] = true;
        uniqDepGraph[key].push(dep);
        if (!remainingSeen.hasOwnProperty(dep)) {
          remainingSeen[dep] = true;
          remaining.push(dep);
        }
      }
    }
  }

  // for each iteration find the keys with the minimum number of dependencies
  // and add them to the result group list
  const groups = [];
  while (remaining.length) {
    let minDepCount = Infinity; let minKeys = []; let minKeyIndexes = []; let affectedKeys = [];
    for (let i = 0; i < remaining.length; i++) {
      let key = remaining[i];
      const deps = uniqDepGraph[key] || [];
      if (deps.length > minDepCount) continue;

      // if (deps.length === minDepCount && !minKeys.some(ea => deps.includes(ea))) {
      if (deps.length === minDepCount && !deps.some(ea => minKeys.includes(ea))) {
        minKeys.push(key);
        minKeyIndexes.push(i);
        affectedKeys.push(...inverseDepGraph[key] || []);
        continue;
      }
      minDepCount = deps.length;
      minKeys = [key];
      minKeyIndexes = [i];
      affectedKeys = (inverseDepGraph[key] || []).slice();
    }
    for (let i = minKeyIndexes.length; i--;) {
      let key = remaining[minKeyIndexes[i]];
      inverseDepGraph[key] = [];
      remaining.splice(minKeyIndexes[i], 1);
    }
    for (let key of affectedKeys) {
      uniqDepGraph[key] = uniqDepGraph[key].filter(ea => !minKeys.includes(ea));
    }
    groups.push(minKeys);
  }
  return groups;
}

/**
 * Starts with `rootNode` and visits all (in)directly related nodes, calling
 * `doFunc` at each node. The result of `doFunc` is passed as first
 * argument to the next iterator call. For the first call the value
 * `carryOver` is used.
 * @param { function(string, *, number): * } doFunc - The function applied to each visited node.
 * @param { Object.<string, string[]> } graph - The graph to traverse.
 * @param { string } rootNode - The id of the node to start the traversal from.
 * @param { * } carryOver - The value to feed into the first call of `doFunc`.
 * @param { Object } context - The object to bind `this` to when calling the `doFunc`.
 * @returns { * } The final result of the reducer.
 * Example:
 * var depGraph = {a: ["b", "c"],b: ["c"]}
 * reduce((_, ea, i) => console.log("%s %s", ea, i), depGraph, "a")
 */
function reduce (doFunc, graph, rootNode, carryOver, ignore, context) {
  let visitedNodes = ignore || []; let index = 0;
  iterator(rootNode);
  return carryOver;

  function iterator (currentNode) {
    if (visitedNodes.indexOf(currentNode) > -1) return;
    carryOver = doFunc.call(context, carryOver, currentNode, index++);
    visitedNodes = visitedNodes.concat([currentNode]);
    const next = withoutAll(graph[currentNode] || [], visitedNodes);
    next.forEach(function (ea) { return iterator(ea); });
  }
}

/**
 * Generates a graph based on a given number of ids that should appear.
 * @param { number } [nKeys=10] - The number of ids inside the generated graph.
 * @returns { Object.<string, string[]> } A graph with the provided number of ids initialized at random.
 */
function random (nKeys = 10) {
  const g = {}; const keys = range(1, nKeys).map(String);
  for (let i = 0; i < keys.length; i++) {
    const r = Math.floor(Math.random() * nKeys);
    g[keys[i]] = shuffle(keys).slice(0, r);
  }
  return g;
}

export {
  clone, without, hull, subgraphReachableBy, invert, sortByReference, reduce,
  random
};