From 5976a88d4078205bbf6706c311c171f73af6d734 Mon Sep 17 00:00:00 2001 From: Lance Welsh Date: Fri, 17 Mar 2017 18:02:57 -0700 Subject: [PATCH 1/4] allow search depth limits by length as well as distance. --- lib/graph.js | 8 ++++---- package.json | 4 ++-- test/runner.js | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/graph.js b/lib/graph.js index b38bcc8..d515ae7 100644 --- a/lib/graph.js +++ b/lib/graph.js @@ -118,7 +118,7 @@ module.exports = (function() { opts = opts || {}; - return this._search(node, opts.compare, opts.count, opts.direction, opts.minDepth, opts.maxDepth); + return this._search(node, opts.compare, opts.count, opts.direction, opts.minDepth, opts.maxDepth, opts.byLength); } @@ -128,11 +128,11 @@ module.exports = (function() { return node === toNode; }; - return this._search(fromNode, passCondition, 1, direction)[0] || new Path([]); + return this._search(fromNode, passCondition, 1, direction)[0] || new Path([], false); } - _search(node, passCondition, count, direction, minDepth, maxDepth) { + _search(node, passCondition, count, direction, minDepth, maxDepth, byLength) { passCondition = (typeof passCondition === 'function') ? passCondition : function(node) { return true; @@ -189,7 +189,7 @@ module.exports = (function() { for (let i = 0, len = edges.length; i < len; i++) { let edge = edges[i]; - let depth = curDepth + edge.distance; + let depth = curDepth + ( byLength ? 1 : edge.distance ); if (maxDepth && depth > maxDepth) { continue; diff --git a/package.json b/package.json index 2c494da..e728b25 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "mocha": "~2.2.4" }, "scripts": { - "test": "mocha ./test/runner.js --harmony_classes", - "check": "node ./test/check.js --harmony_classes" + "test": "mocha ./test/runner.js ", + "check": "node ./test/check.js " }, "repository": { "type": "git", diff --git a/test/runner.js b/test/runner.js index 1349b86..e7ada66 100644 --- a/test/runner.js +++ b/test/runner.js @@ -595,6 +595,23 @@ describe('Test Suite', function() { }); + it('should obey the maxDepth attribute by length', function() { + + let paths = graph.closest(graph.nodes('person').find(2), { + compare: function() { return true; }, + count: 0, + direction: 0, + minDepth: 0, + maxDepth: 1, + byLength: true + }); + + expect(paths.length).to.equal(2); + expect(paths[0].end()).to.equal(graph.nodes('person').find(2)); + expect(paths[1].end()).to.equal(graph.nodes('food').find(1)); + + }); + }); }); From c5b47097cdf6599044bbe5cd76b6c911a0d44047 Mon Sep 17 00:00:00 2001 From: Lance Welsh Date: Fri, 17 Mar 2017 19:51:01 -0700 Subject: [PATCH 2/4] Added compare/passConditions for edges and a stricter one for nodes. but leave the current passCondition alone for backwards compatablity. --- lib/graph.js | 61 +++++++++++++++++++++++++++++++++++++------------- test/runner.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 16 deletions(-) diff --git a/lib/graph.js b/lib/graph.js index d515ae7..73c7db8 100644 --- a/lib/graph.js +++ b/lib/graph.js @@ -1,3 +1,7 @@ +function passConditionTrue() { + return true; +}; + module.exports = (function() { 'use strict'; @@ -118,7 +122,17 @@ module.exports = (function() { opts = opts || {}; - return this._search(node, opts.compare, opts.count, opts.direction, opts.minDepth, opts.maxDepth, opts.byLength); + return this._search( + node, + opts.compare, + opts.compareNode, + opts.compareEdge, + opts.count, + opts.direction, + opts.minDepth, + opts.maxDepth, + opts.byLength + ); } @@ -128,15 +142,25 @@ module.exports = (function() { return node === toNode; }; - return this._search(fromNode, passCondition, 1, direction)[0] || new Path([], false); + return this._search(fromNode, passCondition, null, null, 1, direction)[0] || new Path([], false); } - _search(node, passCondition, count, direction, minDepth, maxDepth, byLength) { - - passCondition = (typeof passCondition === 'function') ? passCondition : function(node) { - return true; - }; + _search( + node, + passCondition, + passConditionNode, + passConditionEdge, + count, + direction, + minDepth, + maxDepth, + byLength + ) { + + passCondition = (typeof passCondition === 'function') ? passCondition : passConditionTrue; // for bw compat + passConditionNode = (typeof passConditionNode === 'function') ? passConditionNode : passConditionTrue; + passConditionEdge = (typeof passConditionEdge === 'function') ? passConditionEdge : passConditionTrue; direction |= 0; count = Math.max(0, count | 0); @@ -189,24 +213,29 @@ module.exports = (function() { for (let i = 0, len = edges.length; i < len; i++) { let edge = edges[i]; - let depth = curDepth + ( byLength ? 1 : edge.distance ); - if (maxDepth && depth > maxDepth) { - continue; - } + if (passConditionEdge(edge)) { + + let depth = curDepth + ( byLength ? 1 : edge.distance ); + + if (maxDepth && depth > maxDepth) { + continue; + } + + let tnode = edges[i].oppositeNode(node); - let tnode = edges[i].oppositeNode(node); + if (!nodePath[tnode.__uniqid__] && passConditionNode(tnode)) { - if (!nodePath[tnode.__uniqid__]) { + nodePath[tnode.__uniqid__] = [edge, tnode]; + enqueue(tnode, depth); - nodePath[tnode.__uniqid__] = [edge, tnode]; - enqueue(tnode, depth); + } } } - if (curDepth >= minDepth && passCondition(node)) { + if (curDepth >= minDepth && passCondition(node) && passConditionNode(node)) { return new Path(getPath(node, nodePath)); } diff --git a/test/runner.js b/test/runner.js index e7ada66..96f8222 100644 --- a/test/runner.js +++ b/test/runner.js @@ -418,11 +418,14 @@ describe('Test Suite', function() { graph.createNode('person', {id: 4, name: 'Kelly'}); graph.createNode('person', {id: 5, name: 'Trevor'}); graph.createNode('person', {id: 6, name: 'Arthur'}); + graph.createNode('person', {id: 7, name: 'Lance'}); graph.createNode('food', {id: 1, name: 'Pizza'}); graph.createNode('food', {id: 2, name: 'Kale'}); graph.createNode('food', {id: 3, name: 'Ice Cream'}); graph.createNode('food', {id: 4, name: 'Meatballs'}); + graph.createNode('food', {id: 5, name: 'Cilantro Salad'}); + graph.createNode('food', {id: 6, name: 'Chocolate Bacon'}); graph.createEdge('likes').link(graph.nodes('person').find(1), graph.nodes('food').find(1)); graph.createEdge('likes').link(graph.nodes('person').find(1), graph.nodes('food').find(4)); @@ -442,6 +445,15 @@ describe('Test Suite', function() { // graph.createEdge('likes').link(graph.nodes('person').find(6), graph.nodes('food').find(3)); graph.createEdge('likes').link(graph.nodes('person').find(6), graph.nodes('food').find(4)); + graph.createEdge('dislikes').link(graph.nodes('person').find(7), graph.nodes('food').find(5)); + graph.createEdge('dislikes').link(graph.nodes('person').find(7), graph.nodes('food').find(6)); + + graph.createEdge('dislikes').link(graph.nodes('person').find(6), graph.nodes('food').find(5)); + graph.createEdge('dislikes').link(graph.nodes('person').find(6), graph.nodes('food').find(6)); + + graph.createEdge('dislikes').link(graph.nodes('person').find(5), graph.nodes('food').find(5)); + graph.createEdge('dislikes').link(graph.nodes('person').find(5), graph.nodes('food').find(6)); + describe('Graph#trace', function() { let nodes = [ @@ -533,7 +545,55 @@ describe('Test Suite', function() { expect(cnodes.indexOf(paths[1].end())).to.be.above(-1); expect(cnodes.indexOf(paths[2].end())).to.be.above(-1); + }); + + it('should obey the compareNode function (all nodes in path must match)', function() { + + let paths = graph.closest(graph.nodes('person').find(2), { + compareNode: function(node) { return node.entity === 'person'; }, + count: 0, + direction: 0, + minDepth: 0, + maxDepth: 0 + }); + + expect(paths.length).to.equal(1); + + expect(paths[0].length()).to.equal(0); + expect(paths[0].end()).to.equal(graph.nodes('person').find(2)); + expect(paths[0].start()).to.equal(graph.nodes('person').find(2)); + + }); + + it('should obey the compareEdge function (all edges in path must match)', function() { + + let paths = graph.closest(graph.nodes('person').find(7), { + compareEdge: function(edge) { return edge.entity === 'dislikes'; }, + count: 0, + direction: 0, + minDepth: 0, + maxDepth: 0 + }); + + expect(paths.length).to.equal(5); + + expect(paths[0].length()).to.equal(0); // person7 to self + + expect(paths[1].length()).to.equal(1); // person7 dislikes food5 + expect(paths[1].start()).to.equal(graph.nodes('person').find(7)); + expect(paths[1].end()).to.equal(graph.nodes('food').find(5)); + + expect(paths[2].length()).to.equal(1); // person7 dislikes food6 + expect(paths[2].start()).to.equal(graph.nodes('person').find(7)); + expect(paths[2].end()).to.equal(graph.nodes('food').find(6)); + + expect(paths[3].length()).to.equal(2); // person7 dislikes the same food as person6 + expect(paths[3].start()).to.equal(graph.nodes('person').find(7)); + expect(paths[3].end()).to.equal(graph.nodes('person').find(6)); + expect(paths[4].length()).to.equal(2); // person7 dislikes the same food as person5 + expect(paths[4].start()).to.equal(graph.nodes('person').find(7)); + expect(paths[4].end()).to.equal(graph.nodes('person').find(5)); }); it('should obey the count attribute', function() { From f27f8d2afe627db1a8d0e9cd3ce810b2d1b8df5b Mon Sep 17 00:00:00 2001 From: Lance Welsh Date: Fri, 17 Mar 2017 21:25:32 -0700 Subject: [PATCH 3/4] Allow Graph's trace to have the same options as closest while keeping backwards copatability with trace taking only direction --- lib/graph.js | 60 +++++++++++------------------------- test/runner.js | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 42 deletions(-) diff --git a/lib/graph.js b/lib/graph.js index 73c7db8..91af34f 100644 --- a/lib/graph.js +++ b/lib/graph.js @@ -1,6 +1,4 @@ -function passConditionTrue() { - return true; -}; +const passConditionTrue = () => true; module.exports = (function() { @@ -122,51 +120,29 @@ module.exports = (function() { opts = opts || {}; - return this._search( - node, - opts.compare, - opts.compareNode, - opts.compareEdge, - opts.count, - opts.direction, - opts.minDepth, - opts.maxDepth, - opts.byLength - ); - + return this._search(node, opts); } trace(fromNode, toNode, direction) { + const compare = node => node === toNode; + const opts = typeof direction === 'object' ? + Object.assign({}, direction, { compare }) : + { compare, direction }; - let passCondition = function(node) { - return node === toNode; - }; - - return this._search(fromNode, passCondition, null, null, 1, direction)[0] || new Path([], false); - + return this._search(fromNode, opts)[0] || new Path([], false); } - _search( - node, - passCondition, - passConditionNode, - passConditionEdge, - count, - direction, - minDepth, - maxDepth, - byLength - ) { - - passCondition = (typeof passCondition === 'function') ? passCondition : passConditionTrue; // for bw compat - passConditionNode = (typeof passConditionNode === 'function') ? passConditionNode : passConditionTrue; - passConditionEdge = (typeof passConditionEdge === 'function') ? passConditionEdge : passConditionTrue; - - direction |= 0; - count = Math.max(0, count | 0); - minDepth = Math.max(0, minDepth | 0); - maxDepth = Math.max(0, maxDepth | 0); - + _search(node, opts) { + const { + compare: passCondition = passConditionTrue, // for bw compat + compareNode: passConditionNode = passConditionTrue, + compareEdge: passConditionEdge = passConditionTrue, + count = 0, + direction = 0, + minDepth = 0, + maxDepth = 0, + byLength = false + } = opts; let nodePath = Object.create(null); nodePath[node.__uniqid__] = [node]; diff --git a/test/runner.js b/test/runner.js index 96f8222..cbf5c79 100644 --- a/test/runner.js +++ b/test/runner.js @@ -492,6 +492,88 @@ describe('Test Suite', function() { }); + describe('Graph#traceObjOpts', function() { + + let nodes = [ + graph.nodes('person').find(1), + graph.nodes('food').find(4), + graph.nodes('person').find(5), + graph.nodes('food').find(2), + graph.nodes('person').find(3) + ]; + + const opts = {} + + let path = graph.trace(graph.nodes('person').find(1), graph.nodes('person').find(3), opts); + + it('should have the correct starting point', function() { + + expect(path.start()).to.equal(nodes[0]); + + }); + + it('should have the correct ending point', function() { + + expect(path.end()).to.equal(nodes[4]); + + }); + + it('should have the correct length', function() { + + expect(path.length()).to.equal(4); + + }); + + it('should have the correct distance', function() { + + expect(path.distance()).to.equal(4); + + }); + + }); + + describe('Graph#traceObjOptsDir', function() { + + let nodes = [ + graph.nodes('person').find(1), + graph.nodes('food').find(4), + graph.nodes('person').find(5), + graph.nodes('food').find(2), + graph.nodes('person').find(3) + ]; + + const opts = { + direction: 1 + } + + let path = graph.trace(graph.nodes('person').find(1), graph.nodes('person').find(3), opts); + + it('should have the correct starting point', function() { + + expect(path.length()).to.be.equal(0); + + }); + + it('should have the correct ending point', function() { + + expect(path.length()).to.be.equal(0); + + }); + + it('should have the correct length', function() { + + expect(path.length()).to.be.equal(0); + + }); + + it('should have the correct distance', function() { + + expect(path.length()).to.be.equal(0); + + }); + + }); + describe('Graph#closest', function() { let paths = graph.closest(graph.nodes('person').find(2), { From 64b8ccc81ece76958fc88fde8bf557927dfe88da Mon Sep 17 00:00:00 2001 From: Lance Welsh Date: Wed, 3 May 2017 09:31:42 -0700 Subject: [PATCH 4/4] Seek some performance improvements --- lib/graph.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/graph.js b/lib/graph.js index 91af34f..404d749 100644 --- a/lib/graph.js +++ b/lib/graph.js @@ -1,4 +1,4 @@ -const passConditionTrue = () => true; +// const passConditionTrue = () => true; module.exports = (function() { @@ -134,9 +134,9 @@ module.exports = (function() { _search(node, opts) { const { - compare: passCondition = passConditionTrue, // for bw compat - compareNode: passConditionNode = passConditionTrue, - compareEdge: passConditionEdge = passConditionTrue, + compare: passCondition, // = passConditionTrue, // for bw compat + compareNode: passConditionNode, // = passConditionTrue, + compareEdge: passConditionEdge, // = passConditionTrue, count = 0, direction = 0, minDepth = 0, @@ -190,7 +190,7 @@ module.exports = (function() { let edge = edges[i]; - if (passConditionEdge(edge)) { + if (!passConditionEdge || passConditionEdge(edge)) { let depth = curDepth + ( byLength ? 1 : edge.distance ); @@ -198,9 +198,9 @@ module.exports = (function() { continue; } - let tnode = edges[i].oppositeNode(node); + let tnode = edge.oppositeNode(node); - if (!nodePath[tnode.__uniqid__] && passConditionNode(tnode)) { + if (!nodePath[tnode.__uniqid__] && (!passConditionNode || passConditionNode(tnode))) { nodePath[tnode.__uniqid__] = [edge, tnode]; enqueue(tnode, depth); @@ -211,7 +211,7 @@ module.exports = (function() { } - if (curDepth >= minDepth && passCondition(node) && passConditionNode(node)) { + if (curDepth >= minDepth && (!passCondition || passCondition(node)) && (!passConditionNode || passConditionNode(node))) { return new Path(getPath(node, nodePath)); }