From 8cb2dc5bc727272f40a226001a8e27a403f57849 Mon Sep 17 00:00:00 2001 From: Floofies Date: Tue, 12 Sep 2017 00:14:06 -0400 Subject: [PATCH 1/7] Added pathfind, added paths, added filter. Added search algorithm parameter in runStrategy. Added custom Stack/Queue. --- CONTRIBUTING.md | 12 +- README.md | 332 ++++++++++++++++++++++++++++++++++++++++++--- spec/Spec.js | 113 +++++++++++++-- src/differentia.js | 303 ++++++++++++++++++++++++++++++++++------- 4 files changed, 682 insertions(+), 78 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d92aae..dfa7b0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,15 +58,21 @@ Your Strategy's `interface` function must call `runStrategy` to use the `iddfs` *Function* ```JavaScript -runStrategy( strategy, parameters ); +runStrategy( strategy, searchAlg, parameters ); ``` -An IOC wrapper to the `iddfs` iterator. (See documentation for `iddfs` in `README.md` for more information.). `runStrategy` advances the `iddfs` iterator and executes Call-With-Current-State callbacks supplied in `strategy`. The state flyweight object is passed to `strategy.main`, which is executed for each element, and `strategy.entry`, which is only executed for the first element. If `strategy.main` returns something other than `undefined`, it will be returned to the caller. +An IOC wrapper to `searchIterator`. (See documentation for `searchIterator` in `README.md` for more information.). `runStrategy` advances the iterator returned by `searchIterator` and executes Call-With-Current-State functions supplied in `strategy`. The state flyweight object is passed to `strategy.main`, which is executed for each element, and `strategy.entry`, which is only executed for the first element. If `strategy.main` returns something other than `undefined`, it will be returned to the caller. + +`searchAlg` is the search algorithm Generator to use; it can be `dfs` or `bfs`, or any Generator. #### Parameters - **`strategy`** Object The strategy Object. +- **`searchAlg`** Generator + + A Generator to use as the search algorthm. + - **`parameters`** Object Avaiable to callbacks via `state.parameters`. It consists of the following properties, but may contain any number of custom properties: @@ -97,7 +103,7 @@ var subject = { strategies.myStrategy = { interface: function (object) { // Here we call "runStrategy" and include our Strategy as parameter 1 - runStrategy(strategies.myStrategy, { + runStrategy(strategies.myStrategy, dfs { subject: object }); }, diff --git a/README.md b/README.md index c835d63..672764f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ This library provides a basic suite of Object/Array focused functions. They are # :closed_book: Documentation ## Functions - [Main Functions](#main-functions) - - [iddfs](#iddfs) + - [dfs](#dfs) + - [bfs](#bfs) - [clone](#clone) - [diffClone](#diffclone) - [diff](#diff) @@ -27,6 +28,9 @@ This library provides a basic suite of Object/Array focused functions. They are - [some](#some) - [every](#every) - [map](#map) + - [filter](#filter) + - [paths](#paths) + - [pathfind](#pathfind) # :page_facing_up: Supported Data Types DataType|Clone|Diff @@ -45,15 +49,15 @@ RegExp|:white_check_mark:|:white_check_mark: # Main Functions -### `iddfs` +### `dfs` -*Iterator Function* +*Generator* ```JavaScript -iddfs( subject [, search = null ] ); +dfs( subject, search = null ] ); ``` -An implementation of [Iterative Deepening Depth-First Search](https://en.wikipedia.org/wiki/Iterative_deepening_depth-first_search). Enumerates properties/elements in `subject`, traversing into any Objects/Arrays, using `search` as a search index. Any properties/nodes present in `search` will be used to enumerate, traverse, and access the properties/nodes of `subject`. If a property/node exists in `search` that does not exist in `subject`, or vice versa, it will be skipped. +An implementation of Depth-First Search. Enumerates properties/elements in `subject`, traversing into any Objects/Arrays, using `search` as a search index. Any properties/nodes present in `search` will be used to enumerate, traverse, and access the properties/nodes of `subject`. If a property/node exists in `search` that does not exist in `subject`, or vice versa, it will be skipped. -Upon calling `next()`, the `iddfs` iterator exposes a single `value` object which encapsulates the state of iteration/traversal at the time of being returned. The object is a flyweight and is thus mutated between every iteration/traversal; because of this, do not attempt to store or otherwise rely on values contained within it. +Upon calling `next()`, the `dfs` iterator exposes a single `value` object which encapsulates the state of iteration/traversal at the time of being returned. The object is a flyweight and is thus mutated between every iteration/traversal; because of this, do not attempt to store or otherwise rely on values contained within it. The `value` object contains the following properties: @@ -62,7 +66,7 @@ Property|Datatype|Description accessor|Mixed|The accessor being used to access `value.tuple.subject` during property/element enumerations. Equal to `state.accessors[state.iteration]`. accessors|Array|An Array of enumerable acessors found in `value.tuple.search`. currentValue|Mixed|The value of the element of enumeration. Equal to `value.tuple.subject[value.accessor]`. -existing|`null` or Object|If `iddfs` encounters an Object/Array it has been before during the same search, this property will be set to the equivalent tuple; otherwise it will be `null`. Objects added to that tuple previously will show up again here. +existing|`null` or Object|If `dfs` encounters an Object/Array it has been before during the same search, this property will be set to the equivalent tuple; otherwise it will be `null`. Objects added to that tuple previously will show up again here. isArray|Boolean|Indicates if the Object being traversed/enumerated is an Array. isContainer|Boolean|Indicates if the current item of the enumeration is an Object or Array. isFirst|Boolean|Indicates if the current item of the enumeration is the first item to be enumerated. @@ -70,6 +74,7 @@ isLast|Boolean|Indicates if the current item of the enumeration is the last item iterations|Number|A number indicating how many items have been enumerated in the current Object/Array. Gets reset to `0` on each traversal. length|Number|The total number of enumerable properties/elements of the current Object/Array being enumerated. noIndex|Boolean|Indicates if a search index was not given. If `true`, then `search` is equal/assigned to `subject`. +targetTuples|Array|A list of tuples to be targeted for traversal. Tuples are removed from the bottom-up. traverse|Boolean|Indicates if the current item of enumeration should be traversed. tuple|Object|An Object containing all Objects being traversed in parallel. @@ -92,7 +97,7 @@ Traversal is performed upon this tuple of objects equally, providing they have o An Object or Array specifying the properties to traverse and enumerate. All other properties are ignored. #### Examples -
Example 1: Using `iddfs` to traverse and enumerate an Object: +
Example 1: Using `dfs` to traverse and enumerate an Object: ```JavaScript var subject = { @@ -104,7 +109,7 @@ var subject = { ] }; -var search = differentia.iddfs(subject, subject); +var search = differentia.dfs(subject, subject); // Starts on the top layer of the root of subject: var iteration = search.next(); @@ -115,7 +120,7 @@ console.log(iteration.value.accessor); // Logs "greetings2" console.log(iteration.value.currentValue); // Logs ["Good Morning!"] // Finished enumerating root Object... -// Now it will traverse and enumerate Objects/Arrays it saw: +// Now it will traverse and enumerate Objects/Arrays it saw in reverse order: iteration = search.next(); console.log(iteration.value.accessor); // Logs 0 console.log(iteration.value.currentValue); // Logs "Good Morning!" @@ -126,7 +131,125 @@ console.log(iteration.value.currentValue); // Logs "Hello World!"
-
Example 2: Using `iddfs` with a search index to traverse and enumerate an Object's *specific* properties: +
Example 2: Using `dfs` with a search index to traverse and enumerate an Object's *specific* properties: + +```JavaScript +var subject = { + greetings1: [ + "Hello World!" + ], + greetings2: [ + "Good Morning!" + ] +}; + +var search = { + greetings2: { + 0: null + } +}; + +var search = differentia.dfs(subject, search); + +// Starts on the top layer of the root of subject: +var iteration = search.next(); +console.log(iteration.value.accessor); // Logs "greetings2" +console.log(iteration.value.currentValue); // Logs ["Good Morning!"] + +// Finished enumerating root Object... +// Now it will traverse and enumerate Objects/Arrays it saw: +iteration = search.next(); +console.log(iteration.value.accessor); // Logs 0 +console.log(iteration.value.currentValue); // Logs "Good Morning!" +``` + +
+ +--- + +### `bfs` + +*Generator* +```JavaScript +bfs( subject [, search = null ] ); +``` +An implementation of Breadth-First Search. Enumerates properties/elements in `subject`, traversing into any Objects/Arrays, using `search` as a search index. Any properties/nodes present in `search` will be used to enumerate, traverse, and access the properties/nodes of `subject`. If a property/node exists in `search` that does not exist in `subject`, or vice versa, it will be skipped. + +Upon calling `next()`, the `bfs` iterator exposes a single `value` object which encapsulates the state of iteration/traversal at the time of being returned. The object is a flyweight and is thus mutated between every iteration/traversal; because of this, do not attempt to store or otherwise rely on values contained within it. + +The `value` object contains the following properties: + +Property|Datatype|Description +---|---|--- +accessor|Mixed|The accessor being used to access `value.tuple.subject` during property/element enumerations. Equal to `state.accessors[state.iteration]`. +accessors|Array|An Array of enumerable acessors found in `value.tuple.search`. +currentValue|Mixed|The value of the element of enumeration. Equal to `value.tuple.subject[value.accessor]`. +existing|`null` or Object|If `bfs` encounters an Object/Array it has been before during the same search, this property will be set to the equivalent tuple; otherwise it will be `null`. Objects added to that tuple previously will show up again here. +isArray|Boolean|Indicates if the Object being traversed/enumerated is an Array. +isContainer|Boolean|Indicates if the current item of the enumeration is an Object or Array. +isFirst|Boolean|Indicates if the current item of the enumeration is the first item to be enumerated. +isLast|Boolean|Indicates if the current item of the enumeration is the last item to be enumerated. +iterations|Number|A number indicating how many items have been enumerated in the current Object/Array. Gets reset to `0` on each traversal. +length|Number|The total number of enumerable properties/elements of the current Object/Array being enumerated. +noIndex|Boolean|Indicates if a search index was not given. If `true`, then `search` is equal/assigned to `subject`. +targetTuples|Array|A list of tuples to be targeted for traversal. Tuples are removed from the bottom-up. +traverse|Boolean|Indicates if the current item of enumeration should be traversed. +tuple|Object|An Object containing all Objects being traversed in parallel. + +The `tuple` object contains the following properties: + +Property|Datatype|Description +---|---|--- +subject|Object/Array|The source of paths/elements for traversal/enumeration. +search|Object/Array|The source of target paths/elements for traversal/enumeration. + +Traversal is performed upon this tuple of objects equally, providing they have overlapping/equal paths. If any node exists in `search` that does not exist in any one object of the tuple, then traversal is aborted for that specific object and it is dropped from the tuple; except if the object lacking the node is `subject`, in which case traversal is aborted completely across all objects of the tuple, and nothing is dropped from the tuple. + +#### Parameters +- **`subject`** Object/Array + + The root Object or Array to enumerate & traverse. + +- **`search`** (*Optional*) Object/Array + + An Object or Array specifying the properties to traverse and enumerate. All other properties are ignored. + +#### Examples +
Example 1: Using `bfs` to traverse and enumerate an Object: + +```JavaScript +var subject = { + greetings1: [ + "Hello World!" + ], + greetings2: [ + "Good Morning!" + ] +}; + +var search = differentia.bfs(subject, subject); + +// Starts on the top layer of the root of subject: +var iteration = search.next(); +console.log(iteration.value.accessor); // Logs "greetings1" +console.log(iteration.value.currentValue); // Logs ["Hello World!"] +iteration = search.next(); +console.log(iteration.value.accessor); // Logs "greetings2" +console.log(iteration.value.currentValue); // Logs ["Good Morning!"] + +// Finished enumerating root Object... +// Now it will traverse and enumerate Objects/Arrays it saw in-order: +iteration = search.next(); +console.log(iteration.value.accessor); // Logs 0 +console.log(iteration.value.currentValue); // Logs "Hello World" +iteration = search.next(); +console.log(iteration.value.accessor); // Logs 0 +console.log(iteration.value.currentValue); // Logs "Good Morning" +``` + +
+ +
Example 2: Using `bfs` with a search index to traverse and enumerate an Object's *specific* properties: ```JavaScript var subject = { @@ -144,7 +267,7 @@ var search = { } }; -var search = differentia.iddfs(subject, search); +var search = differentia.bfs(subject, search); // Starts on the top layer of the root of subject: var iteration = search.next(); @@ -460,6 +583,96 @@ differentia.deepSeal(subject); ___ +### `paths` + +*Function* +```JavaScript +paths( subject [, search = null ] ); +``` +Traverses and enumerates `subject`, recording all paths of the tree. Returns an Array containing Arrays of accessor paths. + +#### Parameters +- **`subject`** Object/Array + + The Object or Array to seal. + +- **`search`** (*Optional*) Object/Array + + An Object or Array specifying the properties to traverse and enumerate. All other properties are ignored. + +#### Examples +
Example 1: Using `paths` to record the paths/branches in an Object: + +```JavaScript +var subject = { + string1: "Pretty", + array1: [ + "Little Clouds", + "Little Trees" + ] +}; + +var paths = differentia.paths(subject); + +console.log(paths); +/* Logs: +[ + ["searchRoot", "string1"], + ["searchRoot", "array1", "0"], + ["searchRoot", "array1", "1"] +] +*/ +``` +
+ +___ + +### `pathfind` + +*Function* +```JavaScript +pathfind( subject, findValue [, search = null ] ); +``` +Traverses and enumerates `subject`, searching for `findValue`. Returns an Array containing the path of `findValue`, or `null` if it was not found. + +#### Parameters +- **`subject`** Object/Array + + The Object or Array to seal. + +- **`findValue`** Object/Array + + The value to find the path of. + +- **`search`** (*Optional*) Object/Array + + An Object or Array specifying the properties to traverse and enumerate. All other properties are ignored. + +#### Examples +
Example 1: Using `paths` to record the paths/branches in an Object: + +```JavaScript +var subject = { + string1: "Pretty", + array1: [ + "Little Clouds", + "Little Trees" + ] +}; + +var path = differentia.pathfind(subject, "Little Trees"); + +console.log(path); +/* Logs: +[ + ["searchRoot", "array1", "1"] +] +*/ +``` +
+ +___ + # Higher-Order Functions ### `forEach` @@ -468,7 +681,7 @@ ___ ```JavaScript forEach( subject , callback [, search = null ] ); ``` -A simple IOC wrapper to the [iddfs](#iddfs) iterator. `callback` is executed for each element. Unlike `Array.prototype.forEach`, this implementation allows a return value of any type, which will be returned to the caller. +A simple IOC wrapper to the [dfs](#dfs) iterator. `callback` is executed for each element. Unlike `Array.prototype.forEach`, this implementation allows a return value of any type, which will be returned to the caller. #### Parameters - **`subject`** Object/Array @@ -538,7 +751,7 @@ differentia.forEach(subject, function (currentValue, accessor, subject) { ```JavaScript find( subject , callback [, search = null ] ); ``` -A simple IOC wrapper to the [iddfs](#iddfs) iterator. `callback` is executed for each element. If `callback` returns `true` at any time, then `currentValue` is immediately returned. If `callback` never returns `true`, then `undefined` is returned. +A simple IOC wrapper to the [dfs](#dfs) iterator. `callback` is executed for each element. If `callback` returns `true` at any time, then `currentValue` is immediately returned. If `callback` never returns `true`, then `undefined` is returned. #### Parameters - **`subject`** Object/Array @@ -602,7 +815,7 @@ console.log(foundValue); // Logs undefined; ```JavaScript some( subject , callback [, search = null ] ); ``` -A simple IOC wrapper to the [iddfs](#iddfs) iterator. `callback` is executed for each element. If `callback` returns `true` at any time, then `true` is immediately returned. If `callback` never returns `true`, then `false` is returned. You can use this function to test if a least one element of the Object tree passes a test. +A simple IOC wrapper to the [dfs](#dfs) iterator. `callback` is executed for each element. If `callback` returns `true` at any time, then `true` is immediately returned. If `callback` never returns `true`, then `false` is returned. You can use this function to test if a least one element of the Object tree passes a test. #### Parameters - **`subject`** Object/Array @@ -659,7 +872,7 @@ console.log(passed); // Logs false, all tests failed. ```JavaScript every( subject , callback [, search = null ] ); ``` -A simple IOC wrapper to the [iddfs](#iddfs) iterator. `callback` is executed for each element. If `callback` returns `false` (or a non-truthy value) at any time, then `false` is immediately returned. If `callback` returns `true` for every element, then `true` is returned. You can use this function to test if all elements of the Object tree pass a test. +A simple IOC wrapper to the [dfs](#dfs) iterator. `callback` is executed for each element. If `callback` returns `false` (or a non-truthy value) at any time, then `false` is immediately returned. If `callback` returns `true` for every element, then `true` is returned. You can use this function to test if all elements of the Object tree pass a test. #### Parameters - **`subject`** Object/Array @@ -716,7 +929,7 @@ console.log(passed); // Logs false, at least one test failed. ```JavaScript map( subject , callback [, search = null ] ); ``` -A simple IOC wrapper to the [iddfs](#iddfs) iterator. Constructs a structural copy of `subject` using the return values of `callback`, which is executed once for each primitive element. +A simple IOC wrapper to the [dfs](#dfs) iterator. Constructs a structural copy of `subject` using the return values of `callback`, which is executed once for each primitive element. #### Parameters - **`subject`** Object/Array @@ -772,4 +985,89 @@ console.log(copy); */ ``` +
+ +--- + +### `filter` + +*Higher-Order Function* +```JavaScript +filter( subject , callback [, search = null ] ); +``` +A simple IOC wrapper to the [bfs](#bfs) iterator. Constructs a structural copy of `subject` using only values/paths which pass the test in `callback`, which is executed once for each primitive element. + +#### Parameters +- **`subject`** Object/Array + + The root Object or Array to enumerate & traverse. + +- **`callback`** Function + + The callback function to execute for each primitive element. If `callback` returns `true`, the current value and node path will be cloned. + +- **`search`** (*Optional*) Object/Array + + An Object or Array specifying the properties to traverse and enumerate. All other properties are ignored. + +#### Callback Parameters +- **`currentValue`** + + The value of the element of enumeration. Equal to `subject[accessor]`. + +- **`accessor`** + + The accessor being used to retrieve `currentValue` from the Object/Array being enumerated. + +- **`subject`** + + The Object/Array being enumerated. + +#### Examples +
Example 1: Using `filter` to only clone Numbers: + +```JavaScript +var subject = { + people: [ + { + name: "Jon Snow", + number: 5555555555 + }, + { + name: "John Madden", + number: 1231231234 + }, + { + name: "Jimmy Neutron", + number: 0001112222 + } + ], + peopleCount: 3 +}; + +// Will clone all numbers and their paths into a new Object +var copy = differentia.filter(subject, function (currentValue, accessor, subject) { + return typeof currentValue === "number"; +}); + +console.log(copy); +// Logs: +/* +{ + "peopleCount": 3, + "people": [ + { + "number": 5555555555 + }, + { + "number": 1231231234 + }, + { + "number": 300178 + } + ] +} +*/ +``` +
\ No newline at end of file diff --git a/spec/Spec.js b/spec/Spec.js index 0293a78..cad7382 100755 --- a/spec/Spec.js +++ b/spec/Spec.js @@ -10,7 +10,8 @@ describe("differentia", function () { var modules = [ 'isContainer', 'getContainerLength', - 'iddfs', + 'bfs', + 'dfs', 'forEach', 'diff', 'clone', @@ -20,7 +21,10 @@ describe("differentia", function () { 'some', 'map', 'deepFreeze', - 'deepSeal' + 'deepSeal', + 'paths', + 'pathfind', + 'filter' ]; it("should contain \"" + modules.join("\", \"") + "\"", function () { modules.forEach(function (moduleName) { @@ -243,26 +247,53 @@ describe("getContainerLength", function () { }); }); -describe("iddfs", function () { +describe("bfs", function () { it("should throw a TypeError when no arguments are given", function () { - expect(() => d.iddfs().next()).toThrow(new TypeError("Argument 1 must be an Object or Array")); + expect(() => d.bfs().next()).toThrow(new TypeError("Argument 1 must be an Object or Array")); }); it("should throw a TypeError when any arguments are the wrong type", function () { - expect(() => d.iddfs(123).next()).toThrow(new TypeError("Argument 1 must be an Object or Array")); - expect(() => d.iddfs("test").next()).toThrow(new TypeError("Argument 1 must be an Object or Array")); - expect(() => d.iddfs({}, 123).next()).toThrow(new TypeError("Argument 2 must be a non-empty Object or Array")); - expect(() => d.iddfs({}, "test").next()).toThrow(new TypeError("Argument 2 must be a non-empty Object or Array")); + expect(() => d.bfs(123).next()).toThrow(new TypeError("Argument 1 must be an Object or Array")); + expect(() => d.bfs("test").next()).toThrow(new TypeError("Argument 1 must be an Object or Array")); + expect(() => d.bfs({}, 123).next()).toThrow(new TypeError("Argument 2 must be a non-empty Object or Array")); + expect(() => d.bfs({}, "test").next()).toThrow(new TypeError("Argument 2 must be a non-empty Object or Array")); }); it("should iterate all nodes and properties", function () { var keyCounts = createKeyCounter(); var testObject = testObjects["Multidimensional Cyclic"]; - var iterator = d.iddfs(testObject, testObject); + var iterator = d.bfs(testObject, testObject); var iteration = iterator.next(); while (!iteration.done) { keyCounts[iteration.value.accessor]++; iteration = iterator.next(); } - //console.info("IDDFS Traversal & Iteration Results:"); + //console.info("bfs Traversal & Iteration Results:"); + for (var accessor in keyCounts) { + //console.info("Accessor \"" + accessor + "\" was visited " + keyCounts[accessor] + " time(s)."); + expect(keyCounts[accessor] > 0).toBe(true); + } + }); +}); + +describe("dfs", function () { + it("should throw a TypeError when no arguments are given", function () { + expect(() => d.dfs().next()).toThrow(new TypeError("Argument 1 must be an Object or Array")); + }); + it("should throw a TypeError when any arguments are the wrong type", function () { + expect(() => d.dfs(123).next()).toThrow(new TypeError("Argument 1 must be an Object or Array")); + expect(() => d.dfs("test").next()).toThrow(new TypeError("Argument 1 must be an Object or Array")); + expect(() => d.dfs({}, 123).next()).toThrow(new TypeError("Argument 2 must be a non-empty Object or Array")); + expect(() => d.dfs({}, "test").next()).toThrow(new TypeError("Argument 2 must be a non-empty Object or Array")); + }); + it("should iterate all nodes and properties", function () { + var keyCounts = createKeyCounter(); + var testObject = testObjects["Multidimensional Cyclic"]; + var iterator = d.dfs(testObject, testObject); + var iteration = iterator.next(); + while (!iteration.done) { + keyCounts[iteration.value.accessor]++; + iteration = iterator.next(); + } + //console.info("bfs Traversal & Iteration Results:"); for (var accessor in keyCounts) { //console.info("Accessor \"" + accessor + "\" was visited " + keyCounts[accessor] + " time(s)."); expect(keyCounts[accessor] > 0).toBe(true); @@ -408,4 +439,66 @@ describe("map", function () { var mapped = [3,5,7,9,11,13]; expect(diff(d.map(start, value => value + 1), mapped)).toBe(false); }); +}); + +describe("paths", function () { + const expectedPaths = [ + ["searchRoot"], + ["searchRoot","0"], + ["searchRoot","1"], + ["searchRoot","0","address"], + ["searchRoot","0","company"], + ["searchRoot","1","address"], + ["searchRoot","1","company"], + ["searchRoot","0","address","geo"], + ["searchRoot","1","address","geo"] + ]; + it("should return an array of all paths", function () { + expect(diff(d.paths(testObjects["Multidimensional Acyclic"]), expectedPaths)).toBe(false); + expect(diff(d.paths(testObjects["Multidimensional Cyclic"]), expectedPaths)).toBe(false); + }); +}); + +describe("pathfind", function () { + it("should return the path of the input if found", function () { + var expectedPath = ["searchRoot", "0", "address", "geo", "lng"]; + expect(diff(d.pathfind(testObjects["Multidimensional Acyclic"], 81.1496), expectedPath)).toBe(false); + expectedPath = ["searchRoot", "1", "company", "name"]; + expect(diff(d.pathfind(testObjects["Multidimensional Acyclic"], "Deckow-Crist"), expectedPath)).toBe(false); + }); + it("should return null if input is not found", function () { + expect(d.pathfind(testObjects["Multidimensional Acyclic"], "This value does not exist!")).toBe(null); + }) +}); + +describe("filter", function () { + it("should clone values which pass the test", function () { + var expectedObject = [ + { + "id": 1, + "address": { + "zipcode": 92998, + "geo": { + "lat": -37.3159, + "lng": 81.1496 + } + } + }, + { + "id": 2, + "address": { + "zipcode": 90566, + "geo": + { + "lat": -43.9509, + "lng": -34.4618} + } + } + ]; + expect(diff(d.filter(testObjects["Multidimensional Acyclic"], value => typeof value === "number"), expectedObject)).toBe(false); + }); + it("should return an empty array is no values pass the test", function () { + var clone = d.filter(testObjects["Multidimensional Acyclic"], value => value === "This value does not exist!"); + expect(Array.isArray(clone) && clone.length === 0).toBe(true); + }); }); \ No newline at end of file diff --git a/src/differentia.js b/src/differentia.js index 4f956bd..2a2b1f8 100755 --- a/src/differentia.js +++ b/src/differentia.js @@ -29,6 +29,11 @@ var differentia = (function () { } } // Thunks to `assert` for method argument type checking. + assert.props = function (input, props, argName) { + for (var prop of props[Symbol.iterator]()) { + assert(prop in input, "Argument " + argName + " must have a \"" + prop + "\" property.", TypeError); + } + }; assert.argType = (boolean, typeString, argName) => assert(boolean, "Argument " + argName + " must be " + typeString, TypeError); assert.string = (input, argName) => assert.argType(typeof input === "string", "a String", argName); assert.function = (input, argName) => assert.argType(typeof input === "function", "a Function", argName); @@ -36,6 +41,88 @@ var differentia = (function () { assert.array = (input, argName) => assert.argType(Array.isArray(input), "an Array", argName); assert.container = (input, argName) => assert.argType(isContainer(input), "an Object or Array", argName); /** + * OffsetArray - An Array wrapper which maintains an offset view of an Array. + * @param {Iterable} [iterable = null] A source iterable to populate the Array with. + */ + function OffsetArray(iterable = null) { + if (iterable !== null) { + this.array = Array.from(iterable); + } else { + this.array = []; + } + this.length = this.array.length; + this.index0 = 0; + } + /** + * OffsetArray.prototype.item - Returns a value by it's index, or `undefined` if it does not exist. + * @param {Number} index + * @returns {any} + */ + OffsetArray.prototype.item = function (index) { + return this.array[this.index0 + Number(index)]; + }; + /** + * OffsetArray.prototype.set - Assigns a value to an index. + * @param {Number} index + * @param {any} value + */ + OffsetArray.prototype.set = function (index, value) { + const newIndex = this.index0 + Number(index); + if (newIndex >= this.length) { + this.length = newIndex + 1; + } + this.array[newIndex] = value; + }; + /** + * OffsetArray.prototype.shift - Removes and returns the first element. + * @returns {any} + */ + OffsetArray.prototype.shift = function () { + if (this.length !== 0) { + this.length--; + this.index0++; + return this.array[this.index0 - 1]; + } + return undefined; + }; + /** + * OffsetArray.prototype.pop - Removes and returns the last element. + * @returns {any} + */ + OffsetArray.prototype.pop = function () { + if (this.length !== 0) { + this.length--; + return this.array[this.index0 + this.length]; + } + return undefined; + }; + /** + * OffsetArray.prototype.push - Adds a value to the end of the Array. + * @param {any} value + */ + OffsetArray.prototype.push = function (value) { + this.array[this.index0 + this.length] = value; + this.length++; + }; + /** + * Queue - Wraps OffsetArray, providing `OffsetArray.prototype.shift` as a `take` property. + * @param {Iterable} [iterable = null] A source iterable to populate the Array with. + */ + function Queue(iterable = null) { + OffsetArray.call(this, iterable); + this.take = OffsetArray.prototype.shift; + } + Queue.prototype = OffsetArray.prototype; + /** + * Stack - Wraps OffsetArray, providing `OffsetArray.prototype.pop` as a `take` property. + * @param {Iterable} [iterable = null] A source iterable to populate the Array with. + */ + function Stack(iterable = null) { + OffsetArray.call(this, iterable); + this.take = OffsetArray.prototype.pop; + } + Stack.prototype = OffsetArray.prototype; + /** * isContainer - Returns `true` if `input` is an Object or Array, otherwise returns `false`. * @param {any} input * @returns {Boolean} @@ -90,24 +177,15 @@ var differentia = (function () { } throw new TypeError("The given parameter must be an Object or Array"); } - /** - * iddfs - An iterator implementation of Iterative Deepening Depth-First Search - * Returns an Iterator usable with `next()`. - * @param {Object|Array} subject The Object/Array to access. - * @param {Object|Array|null} [search = null] The Object/Array used to target accessors in `subject` - * @returns {Iterator} - */ - function* iddfs(subject, search = null) { - assert.container(subject, 1); - assert.argType(search === null || (isContainer(search) && getContainerLength(search) > 0), "a non-empty Object or Array", 2); - var state = { + function createIterationState() { + return { accessors: null, traverse: true, tuple: {}, existing: null, isContainer: false, - isArray: false, noIndex: false, + targetTuples: [], length: 0, iterations: 0, isLast: false, @@ -115,6 +193,19 @@ var differentia = (function () { accessor: null, currentValue: null }; + } + /** + * searchIterator - An adaptable graph search algorithm + * Returns an Iterator usable with `next()`. + * @param {Object|Array} subject The Object/Array to access. + * @param {Queue|Stack} targetTuples An instance of `Queue` or `Stack` to store target nodes in. + * @param {Object|Array|null} [search = null] The Object/Array used to target accessors in `subject` + * @returns {Iterator} + */ + function* searchIterator(subject, targetTuples, search = null) { + assert.container(subject, 1); + assert.argType(search === null || (isContainer(search) && getContainerLength(search) > 0), "a non-empty Object or Array", 2); + var state = createIterationState(); if (search === null) { search = subject; } @@ -125,25 +216,22 @@ var differentia = (function () { state.searchRoot = search; // Unique Node Map var nodeMap = new Map(); - // Object Traversal Stack - var nodeStack = []; // Add Root Tuple to Stack - nodeStack[0] = { + state.targetTuples = targetTuples; + state.targetTuples.push({ search: search, subject: subject - }; - nodeMap.set(state.searchRoot, nodeStack[0]); - // Iterate `nodeStack` - _traverse: while (nodeStack.length > 0) { + }); + nodeMap.set(state.searchRoot, state.targetTuples.item(0)); + // Iterate `state.targetTuples` + _traverse: while (state.targetTuples.length > 0) { // Traverse `search`, iterating through it's properties. _iterate: for ( - // Pop last item from Stack - state.tuple = nodeStack.pop(), + state.tuple = state.targetTuples.take(), state.iterations = 0, - state.isArray = Array.isArray(state.tuple.search), - state.accessors = Object.keys(state.tuple.search), - state.length = state.accessors.length; + state.accessors = Object.keys(state.tuple.search); state.accessor = state.accessors[state.iterations], + state.length = state.accessors.length, state.iterations < state.length; state.iterations++ ) { @@ -155,23 +243,23 @@ var differentia = (function () { } // If the value is a container, hasn't been seen before, and has enumerables, then we can traverse it. state.traverse = state.isContainer && state.existing === null && getContainerLength(state.tuple.search[state.accessor]) > 0; - // If the value can't be traversed, nodeStack is empty, and we are on the last enumerable, we know we're on the last iteration. + // If the value can't be traversed, state.targetTuples is empty, and we are on the last enumerable, we know we're on the last iteration. // I call this "Terminating Tail-Edge Preemption" since the generator will terminate after this last yield. - state.isLast = (!state.isContainer || !state.traverse) && (state.iterations === state.length - 1 && nodeStack.length === 0); + state.isLast = !state.traverse && state.iterations === state.length - 1 && state.targetTuples.length === 0; state.currentValue = state.tuple.subject[state.accessor]; try { // Yield the Shared State Object yield state; } catch (exception) { - console.error("An error occured while traversing \"" + loc + "\" at node depth " + nodeStack.length + ":"); + console.error("An error occured while traversing \"" + loc + "\" at node depth " + state.targetTuples.length + ":"); console.error(exception); console.info("Node Traversal Stack:"); - console.info(nodeStack); + console.info(state.targetTuples); } if (state.isFirst) { state.isFirst = false; } - if (!state.isContainer || !state.traverse || (!state.noIndex && !(state.accessor in state.tuple.subject))) { + if (!state.traverse || (!state.noIndex && !(state.accessor in state.tuple.subject))) { continue _iterate; } // Node has not been seen before, so traverse it @@ -185,24 +273,45 @@ var differentia = (function () { // Save the Tuple to `nodeMap` nodeMap.set(state.tuple.search[state.accessor], nextTuple); // Push the next Tuple into the stack - nodeStack[nodeStack.length] = nextTuple; + state.targetTuples.push(nextTuple); } } + } + /** + * dfs - A thunk to `searchIterator`, providing a Stack for target nodes. + * Causes `seatchIterator` to behave as Depth-First Search. + * @param {Object|Array} subject The Object/Array to access. + * @param {Object|Array|null} [search = null] The Object/Array used to target accessors in `subject` + * @returns {Iterator} + */ + function dfs(subject, search = null) { + return searchIterator(subject, new Stack(), search); + } + /** + * bfs - A thunk to `searchIterator`, providing a Queue for target nodes. + * Causes `seatchIterator` to behave as Breadth-First Search. + * @param {Object|Array} subject The Object/Array to access. + * @param {Object|Array|null} [search = null] The Object/Array used to target accessors in `subject` + * @returns {Iterator} + */ + function bfs(subject, search = null) { + return searchIterator(subject, new Queue(), search); } /** - * runStrategy - Calls `strategy.entry` and `strategy.main` with the state of the iddfs iterator. + * runStrategy - Calls `strategy.entry` and `strategy.main` with the state of the search iterator. * `strategy.entry` is optional. It is only executed once, for the first value the iterator yields. * @param {Object} strategy An Object containing an optional `entry` property and a required `main` property. + * @param {Generator} searchAlg A Generator to use as the search algorthm. * @param {Object} parameters An Object containing a required `subject` property, and an optional `search` property. * @returns {Mixed} Returns anything `strategy.main` returns. */ - function runStrategy(strategy, parameters) { + function runStrategy(strategy, searchAlg, parameters) { assert.object(strategy, 1); - assert("main" in strategy, "Parameter 1 must have a \"main\" property.", TypeError); - assert.object(parameters, 2); - assert("subject" in parameters, "Parameter 2 must have a \"subject\" property.", TypeError); + assert.props(strategy, ["main"], 1); + assert.object(parameters, 3); + assert.props(parameters, ["subject"], 3); // Initialize search algorithm. - var iterator = iddfs(parameters.subject, parameters.search); + var iterator = searchAlg(parameters.subject, parameters.search); var iteration = iterator.next(); var state = iteration.value; // Save parameters in a prop the strategy can see @@ -225,7 +334,7 @@ var differentia = (function () { const strategies = {}; strategies.clone = { interface: function (subject, search = null) { - return runStrategy(strategies.clone, { + return runStrategy(strategies.clone, dfs, { subject: subject, search: search }); @@ -270,7 +379,7 @@ var differentia = (function () { if (search === null && getContainerLength(subject) !== getContainerLength(compare)) { return true; } - return runStrategy(strategies.diff, { + return runStrategy(strategies.diff, dfs, { subject: subject, compare: compare, search: search @@ -312,7 +421,7 @@ var differentia = (function () { }; strategies.diffClone = { interface: function (subject, compare, search = null) { - return runStrategy(strategies.diffClone, { + return runStrategy(strategies.diffClone, dfs, { subject: subject, compare: compare, search: search @@ -330,7 +439,7 @@ var differentia = (function () { }; strategies.deepFreeze = { interface: function (subject, search = null) { - return runStrategy(strategies.deepFreeze, { + return runStrategy(strategies.deepFreeze, dfs, { subject: subject, search: search }); @@ -349,7 +458,7 @@ var differentia = (function () { }; strategies.deepSeal = { interface: function (subject, search = null) { - return runStrategy(strategies.deepSeal, { + return runStrategy(strategies.deepSeal, dfs, { subject: subject, search: search }); @@ -368,7 +477,7 @@ var differentia = (function () { }; strategies.forEach = { interface: function (subject, callback, search = null) { - return runStrategy(strategies.forEach, { + return runStrategy(strategies.forEach, dfs, { subject: subject, search: search, callback: callback @@ -380,7 +489,7 @@ var differentia = (function () { }; strategies.find = { interface: function (subject, callback, search = null) { - return runStrategy(strategies.find, { + return runStrategy(strategies.find, dfs, { subject: subject, search: search, callback: callback @@ -394,7 +503,7 @@ var differentia = (function () { }; strategies.some = { interface: function (subject, callback, search = null) { - return runStrategy(strategies.some, { + return runStrategy(strategies.some, dfs, { subject: subject, search: search, callback: callback @@ -411,7 +520,7 @@ var differentia = (function () { }; strategies.every = { interface: function (subject, callback, search = null) { - return runStrategy(strategies.every, { + return runStrategy(strategies.every, dfs, { subject: subject, search: search, callback: callback @@ -428,7 +537,7 @@ var differentia = (function () { }; strategies.map = { interface: function (subject, callback, search = null) { - return runStrategy(strategies.map, { + return runStrategy(strategies.map, dfs, { subject: subject, search: search, callback: callback @@ -446,10 +555,108 @@ var differentia = (function () { } } }; + strategies.paths = { + interface: function (subject, search = null) { + return runStrategy(strategies.paths, bfs, { + subject: subject, + search, search + }); + }, + entry: function (state) { + state.paths = [["searchRoot"]]; + state.currentPath = state.paths[0]; + }, + main: function (state) { + if (state.iterations === 0) { + state.currentPath = state.paths[state.paths.length - (state.targetTuples.length + 1)]; + } + if (state.isLast) { + return state.paths; + } + if (state.traverse) { + state.paths[state.paths.length] = Array.from(state.paths[state.paths.length - (state.targetTuples.length + 1)]); + state.paths[state.paths.length - 1][state.paths[state.paths.length - 1].length] = state.accessor; + } + } + }; + strategies.pathfind = { + interface: function (subject, findValue, search = null) { + return runStrategy(strategies.pathfind, bfs, { + subject: subject, + search: search, + findValue: findValue + }); + }, + entry: strategies.paths.entry, + main: function (state) { + strategies.paths.main(state); + if (state.currentValue === state.parameters.findValue) { + state.currentPath.push(state.accessor); + return state.currentPath; + } + if (state.isLast) { + return null; + } + } + }; + strategies.filter = { + interface: function (subject, callback, search = null) { + return runStrategy(strategies.filter, bfs, { + subject: subject, + search: search, + callback: callback + }); + }, + entry: function (state) { + strategies.clone.entry(state); + strategies.paths.entry(state); + state.pendingPaths = []; + }, + main: function (state) { + strategies.paths.main(state); + if (!state.isContainer && strategies.forEach.main(state)) { + state.pendingPaths[state.pendingPaths.length] = Array.from(state.currentPath); + state.pendingPaths[state.pendingPaths.length - 1].push(state.accessor); + } + if (state.isLast) { + while (state.pendingPaths.length > 0) { + var path = state.pendingPaths.shift(); + var nodeQueue = [{ + subject: state.subjectRoot, + clone: state.cloneRoot + }]; + while (path.length > 0 && nodeQueue.length > 0) { + var accessor = path.shift(); + if (accessor === "searchRoot") { + continue; + } + var tuple = nodeQueue.shift(); + if (!(accessor in tuple.clone)) { + if (path.length === 0) { + tuple.clone[accessor] = tuple.subject[accessor]; + } else { + tuple.clone[accessor] = createContainer(tuple.subject[accessor]); + } + } + if (path.length === 0) { + continue; + } + var nextTuple = {}; + for (var unit in tuple) { + nextTuple[unit] = tuple[unit][accessor]; + } + nodeQueue[nodeQueue.length] = nextTuple; + } + } + return state.cloneRoot; + } + } + } // Reveal Modules var publicModules = {}; - // Add some extra functions which are not iddfs strategies - publicModules.iddfs = iddfs; + // Add some extra functions which are not search strategies + publicModules.dfs = dfs; + publicModules.bfs = bfs; publicModules.getContainerLength = getContainerLength; publicModules.isContainer = isContainer; // Automatically Reveal Strategy Interfaces From 1c2a0c6f0704aae60a98db3fbf1c19a4fcf3e810 Mon Sep 17 00:00:00 2001 From: Floofies Date: Tue, 12 Sep 2017 23:42:56 -0400 Subject: [PATCH 2/7] Refinements --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfa7b0e..0ef8613 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,7 @@ Your Strategy's `interface` function must call `runStrategy` to use the `iddfs` ```JavaScript runStrategy( strategy, searchAlg, parameters ); ``` -An IOC wrapper to `searchIterator`. (See documentation for `searchIterator` in `README.md` for more information.). `runStrategy` advances the iterator returned by `searchIterator` and executes Call-With-Current-State functions supplied in `strategy`. The state flyweight object is passed to `strategy.main`, which is executed for each element, and `strategy.entry`, which is only executed for the first element. If `strategy.main` returns something other than `undefined`, it will be returned to the caller. +An IOC wrapper for Generators/Iterators. `runStrategy` advances the iterator returned by `searchIterator` and executes Call-With-Current-State functions supplied in `strategy`. The state flyweight object is passed to `strategy.main`, which is executed for each element, and `strategy.entry`, which is only executed for the first element. If `strategy.main` returns something other than `undefined`, it will be returned to the caller. `searchAlg` is the search algorithm Generator to use; it can be `dfs` or `bfs`, or any Generator. @@ -71,7 +71,7 @@ An IOC wrapper to `searchIterator`. (See documentation for `searchIterator` in ` - **`searchAlg`** Generator - A Generator to use as the search algorthm. + A Generator to use as the search algorthm; it can be `dfs` or `bfs`, or any Generator. - **`parameters`** Object @@ -122,7 +122,7 @@ strategies.myStrategy.interface(subject); // We can now see the Primitives were overwritten with "Hello World". console.log(subject); -/* +/* Logs: "{ greetings1: [ "Hello World" From a2149ac8a135c0966ac4a451828c199aa6cff2c9 Mon Sep 17 00:00:00 2001 From: Floofies Date: Tue, 12 Sep 2017 23:43:20 -0400 Subject: [PATCH 3/7] Refined docs --- src/differentia.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/differentia.js b/src/differentia.js index 2a2b1f8..1956c0b 100755 --- a/src/differentia.js +++ b/src/differentia.js @@ -74,7 +74,7 @@ var differentia = (function () { this.array[newIndex] = value; }; /** - * OffsetArray.prototype.shift - Removes and returns the first element. + * OffsetArray.prototype.shift - Returns the first element and excludes it from the view. * @returns {any} */ OffsetArray.prototype.shift = function () { @@ -86,7 +86,7 @@ var differentia = (function () { return undefined; }; /** - * OffsetArray.prototype.pop - Removes and returns the last element. + * OffsetArray.prototype.pop - Returns the last element and excludes it from the view. * @returns {any} */ OffsetArray.prototype.pop = function () { @@ -97,7 +97,7 @@ var differentia = (function () { return undefined; }; /** - * OffsetArray.prototype.push - Adds a value to the end of the Array. + * OffsetArray.prototype.push - Adds a value to the end of the view. * @param {any} value */ OffsetArray.prototype.push = function (value) { From 27e784edae2c33d8ded5cc212a10cbe09b752480 Mon Sep 17 00:00:00 2001 From: Floofies Date: Mon, 9 Oct 2017 10:36:21 -0400 Subject: [PATCH 4/7] Restructured a little, fixed function TOC misplacement --- README.md | 56 +++++++++++++++---------------------------------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 672764f..82264f2 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ This library provides a basic suite of Object/Array focused functions. They are - [diff](#diff) - [deepFreeze](#deepfreeze) - [deepSeal](#deepseal) + - [paths](#paths) + - [pathfind](#pathfind) - [Higher-Order Functions](#higher-order-functions) - [forEach](#foreach) - [find](#find) @@ -29,8 +31,6 @@ This library provides a basic suite of Object/Array focused functions. They are - [every](#every) - [map](#map) - [filter](#filter) - - [paths](#paths) - - [pathfind](#pathfind) # :page_facing_up: Supported Data Types DataType|Clone|Diff @@ -49,17 +49,11 @@ RegExp|:white_check_mark:|:white_check_mark: # Main Functions -### `dfs` +## Search Algorithms -*Generator* -```JavaScript -dfs( subject, search = null ] ); -``` -An implementation of Depth-First Search. Enumerates properties/elements in `subject`, traversing into any Objects/Arrays, using `search` as a search index. Any properties/nodes present in `search` will be used to enumerate, traverse, and access the properties/nodes of `subject`. If a property/node exists in `search` that does not exist in `subject`, or vice versa, it will be skipped. +The search iterators, `bfs` and `dfs`, are actually both the same `searchIterator` algorithm (See CONTRIBUTING.md for more details about `searchIterator`) with differing traversal scheduling data structures (Queue VS Stack). -Upon calling `next()`, the `dfs` iterator exposes a single `value` object which encapsulates the state of iteration/traversal at the time of being returned. The object is a flyweight and is thus mutated between every iteration/traversal; because of this, do not attempt to store or otherwise rely on values contained within it. - -The `value` object contains the following properties: +Upon calling `next()`, the search iterators expose a single state object in `value` which encapsulates the current state of iteration/traversal. The object is a flyweight and is thus mutated between every iteration/traversal; because of this, do not attempt to store or otherwise rely on values contained within it for more than one step in the iteration. Property|Datatype|Description ---|---|--- @@ -87,6 +81,16 @@ search|Object/Array|The source of target paths/elements for traversal/enumeratio Traversal is performed upon this tuple of objects equally, providing they have overlapping/equal paths. If any node exists in `search` that does not exist in any one object of the tuple, then traversal is aborted for that specific object and it is dropped from the tuple; except if the object lacking the node is `subject`, in which case traversal is aborted completely across all objects of the tuple, and nothing is dropped from the tuple. +### `dfs` + +*Generator* +```JavaScript +dfs( subject, search = null ] ); +``` +An implementation of Depth-First Search. Enumerates properties/elements in `subject`, traversing into any Objects/Arrays, using `search` as a search index. Any properties/nodes present in `search` will be used to enumerate, traverse, and access the properties/nodes of `subject`. If a property/node exists in `search` that does not exist in `subject`, or vice versa, it will be skipped. + +Upon calling `next()`, the `dfs` iterator exposes a single `value` object which encapsulates the state of iteration/traversal at the time of being returned. The object is a flyweight and is thus mutated between every iteration/traversal; because of this, do not attempt to store or otherwise rely on values contained within it. + #### Parameters - **`subject`** Object/Array @@ -175,36 +179,6 @@ bfs( subject [, search = null ] ); ``` An implementation of Breadth-First Search. Enumerates properties/elements in `subject`, traversing into any Objects/Arrays, using `search` as a search index. Any properties/nodes present in `search` will be used to enumerate, traverse, and access the properties/nodes of `subject`. If a property/node exists in `search` that does not exist in `subject`, or vice versa, it will be skipped. -Upon calling `next()`, the `bfs` iterator exposes a single `value` object which encapsulates the state of iteration/traversal at the time of being returned. The object is a flyweight and is thus mutated between every iteration/traversal; because of this, do not attempt to store or otherwise rely on values contained within it. - -The `value` object contains the following properties: - -Property|Datatype|Description ----|---|--- -accessor|Mixed|The accessor being used to access `value.tuple.subject` during property/element enumerations. Equal to `state.accessors[state.iteration]`. -accessors|Array|An Array of enumerable acessors found in `value.tuple.search`. -currentValue|Mixed|The value of the element of enumeration. Equal to `value.tuple.subject[value.accessor]`. -existing|`null` or Object|If `bfs` encounters an Object/Array it has been before during the same search, this property will be set to the equivalent tuple; otherwise it will be `null`. Objects added to that tuple previously will show up again here. -isArray|Boolean|Indicates if the Object being traversed/enumerated is an Array. -isContainer|Boolean|Indicates if the current item of the enumeration is an Object or Array. -isFirst|Boolean|Indicates if the current item of the enumeration is the first item to be enumerated. -isLast|Boolean|Indicates if the current item of the enumeration is the last item to be enumerated. -iterations|Number|A number indicating how many items have been enumerated in the current Object/Array. Gets reset to `0` on each traversal. -length|Number|The total number of enumerable properties/elements of the current Object/Array being enumerated. -noIndex|Boolean|Indicates if a search index was not given. If `true`, then `search` is equal/assigned to `subject`. -targetTuples|Array|A list of tuples to be targeted for traversal. Tuples are removed from the bottom-up. -traverse|Boolean|Indicates if the current item of enumeration should be traversed. -tuple|Object|An Object containing all Objects being traversed in parallel. - -The `tuple` object contains the following properties: - -Property|Datatype|Description ----|---|--- -subject|Object/Array|The source of paths/elements for traversal/enumeration. -search|Object/Array|The source of target paths/elements for traversal/enumeration. - -Traversal is performed upon this tuple of objects equally, providing they have overlapping/equal paths. If any node exists in `search` that does not exist in any one object of the tuple, then traversal is aborted for that specific object and it is dropped from the tuple; except if the object lacking the node is `subject`, in which case traversal is aborted completely across all objects of the tuple, and nothing is dropped from the tuple. - #### Parameters - **`subject`** Object/Array From 86ac5e2b079a45df3abf0f020a71fd4e59dd5a10 Mon Sep 17 00:00:00 2001 From: Floofies Date: Tue, 10 Oct 2017 00:14:06 -0400 Subject: [PATCH 5/7] Added diffPaths --- spec/Spec.js | 63 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/spec/Spec.js b/spec/Spec.js index cad7382..21793bb 100755 --- a/spec/Spec.js +++ b/spec/Spec.js @@ -23,7 +23,8 @@ describe("differentia", function () { 'deepFreeze', 'deepSeal', 'paths', - 'pathfind', + 'pathFind', + 'diffPaths', 'filter' ]; it("should contain \"" + modules.join("\", \"") + "\"", function () { @@ -459,18 +460,68 @@ describe("paths", function () { }); }); -describe("pathfind", function () { +describe("pathFind", function () { it("should return the path of the input if found", function () { var expectedPath = ["searchRoot", "0", "address", "geo", "lng"]; - expect(diff(d.pathfind(testObjects["Multidimensional Acyclic"], 81.1496), expectedPath)).toBe(false); + expect(diff(d.pathFind(testObjects["Multidimensional Acyclic"], 81.1496), expectedPath)).toBe(false); expectedPath = ["searchRoot", "1", "company", "name"]; - expect(diff(d.pathfind(testObjects["Multidimensional Acyclic"], "Deckow-Crist"), expectedPath)).toBe(false); + expect(diff(d.pathFind(testObjects["Multidimensional Acyclic"], "Deckow-Crist"), expectedPath)).toBe(false); }); it("should return null if input is not found", function () { - expect(d.pathfind(testObjects["Multidimensional Acyclic"], "This value does not exist!")).toBe(null); + expect(d.pathFind(testObjects["Multidimensional Acyclic"], "This value does not exist!")).toBe(null); }) }); +describe("diffPaths", function () { + it("should return an array of paths that differ", function () { + var expectedPaths = [ + ["searchRoot","0"], + ["searchRoot","1"], + ["searchRoot","0","id"], + ["searchRoot","0","name"], + ["searchRoot","0","username"], + ["searchRoot","0","email"], + ["searchRoot","0","regex"], + ["searchRoot","0","address"], + ["searchRoot","0","website"], + ["searchRoot","0","company"], + ["searchRoot","0","otherUser"], + ["searchRoot","1","id"], + ["searchRoot","1","name"], + ["searchRoot","1","username"], + ["searchRoot","1","email"], + ["searchRoot","1","regex"], + ["searchRoot","1","address"], + ["searchRoot","1","website"], + ["searchRoot","1","company"], + ["searchRoot","1","otherUser"], + ["searchRoot","0","address","street"], + ["searchRoot","0","address","suite"], + ["searchRoot","0","address","city"], + ["searchRoot","0","address","zipcode"], + ["searchRoot","0","address","geo"], + ["searchRoot","0","company","active"], + ["searchRoot","0","company","name"], + ["searchRoot","0","company","catchPhrase"], + ["searchRoot","0","company","bs"], + ["searchRoot","1","address","street"], + ["searchRoot","1","address","suite"], + ["searchRoot","1","address","city"], + ["searchRoot","1","address","zipcode"], + ["searchRoot","1","address","geo"], + ["searchRoot","1","company","active"], + ["searchRoot","1","company","name"], + ["searchRoot","1","company","catchPhrase"], + ["searchRoot","1","company","bs"], + ["searchRoot","0","address","geo","lat"], + ["searchRoot","0","address","geo","lng"], + ["searchRoot","1","address","geo","lat"], + ["searchRoot","1","address","geo","lng"] + ]; + expect(diff(differentia.diffPaths(testObjects["Multidimensional Cyclic"], testObjects["Linear Acyclic"]), expectedPaths)).toBe(false); + }); +}); + describe("filter", function () { it("should clone values which pass the test", function () { var expectedObject = [ @@ -497,7 +548,7 @@ describe("filter", function () { ]; expect(diff(d.filter(testObjects["Multidimensional Acyclic"], value => typeof value === "number"), expectedObject)).toBe(false); }); - it("should return an empty array is no values pass the test", function () { + it("should return an empty array if no values pass the test", function () { var clone = d.filter(testObjects["Multidimensional Acyclic"], value => value === "This value does not exist!"); expect(Array.isArray(clone) && clone.length === 0).toBe(true); }); From 82f196f74f62819325d5b1720f4705628e4a45cf Mon Sep 17 00:00:00 2001 From: Floofies Date: Tue, 10 Oct 2017 00:14:38 -0400 Subject: [PATCH 6/7] Added diffPaths and restructred --- README.md | 86 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 82264f2..facd92e 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,12 @@ === This library provides a basic suite of Object/Array focused functions. They are all "deep" algorithms, and fully traverse all child Objects/Arrays/properties unless given a search index object with specifies otherwise. -- Deep Object Cloning -- Deep Object Diffing -- Deep Freezing/Sealing -- Differential Deep Object Cloning -- A small compliment of higher-order functions. - ---- - # :closed_book: Documentation -## Functions -- [Main Functions](#main-functions) + +- [Search Algorithm Iterators](#search-algorithm-iterators) - [dfs](#dfs) - [bfs](#bfs) +- [Main Functions](#main-functions) - [clone](#clone) - [diffClone](#diffclone) - [diff](#diff) @@ -24,6 +17,7 @@ This library provides a basic suite of Object/Array focused functions. They are - [deepSeal](#deepseal) - [paths](#paths) - [pathfind](#pathfind) + - [diffpaths](#diffpaths) - [Higher-Order Functions](#higher-order-functions) - [forEach](#foreach) - [find](#find) @@ -32,7 +26,9 @@ This library provides a basic suite of Object/Array focused functions. They are - [map](#map) - [filter](#filter) -# :page_facing_up: Supported Data Types +--- + +## :page_facing_up: Supported Data Types DataType|Clone|Diff ---|---|--- Function|:x:|:x: @@ -47,9 +43,7 @@ RegExp|:white_check_mark:|:white_check_mark: --- -# Main Functions - -## Search Algorithms +## Search Algorithm Iterators The search iterators, `bfs` and `dfs`, are actually both the same `searchIterator` algorithm (See CONTRIBUTING.md for more details about `searchIterator`) with differing traversal scheduling data structures (Queue VS Stack). @@ -259,6 +253,8 @@ console.log(iteration.value.currentValue); // Logs "Good Morning!" --- +# Main Functions + ### `clone` *Function* @@ -563,12 +559,12 @@ ___ ```JavaScript paths( subject [, search = null ] ); ``` -Traverses and enumerates `subject`, recording all paths of the tree. Returns an Array containing Arrays of accessor paths. +Traverses and enumerates `subject`, returning an array listing all paths of the tree. #### Parameters - **`subject`** Object/Array - The Object or Array to seal. + The Object or Array to search. - **`search`** (*Optional*) Object/Array @@ -612,7 +608,7 @@ Traverses and enumerates `subject`, searching for `findValue`. Returns an Array #### Parameters - **`subject`** Object/Array - The Object or Array to seal. + The Object or Array to search. - **`findValue`** Object/Array @@ -645,6 +641,62 @@ console.log(path); ```
+--- + +### `diffPaths` + +*Function* +```JavaScript +diffPaths( subject, compare [, search = null ] ); +``` +Traverses and enumerates `subject`, returning an array listing all paths of the tree which differ from the paths of `compare`. + +## Parameters +- **`subject`** Object/Array + + The Object or Array to search. + +- **`compared`** Object/Array + + The Object or Array to compare to `subject`. + +- **`search`** (*Optional*) Object/Array + + An Object or Array specifying the properties to traverse and enumerate. All other properties are ignored. + +## Examples +
Example 1: Using `paths` to find differing paths/branches: + +```JavaScript +var subject = { + string1: "Pretty", + array1: [ + "Little Clouds", + "Little Trees" + ] +}; + +var compared = { + string2: "Pretty", + array1: [ + "Little Branches", + "Little Leaves" + ] +}; + +var differingPaths = differentia.diffPaths(subject, compare); + +console.log(differingPaths); +/* Logs: +[ + ["searchRoot","string1"], + ["searchRoot","array1","0"], + ["searchRoot","array1","1"] +] +*/ +``` +
+ ___ # Higher-Order Functions From a3c1093bd1d8adc3c6d4806efeec7d78aa48bd24 Mon Sep 17 00:00:00 2001 From: Floofies Date: Tue, 10 Oct 2017 00:15:28 -0400 Subject: [PATCH 7/7] Fixed bug in traversal step when extra targets are supplied by a strategy --- src/differentia.js | 50 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/differentia.js b/src/differentia.js index 1956c0b..fb64f85 100755 --- a/src/differentia.js +++ b/src/differentia.js @@ -177,6 +177,10 @@ var differentia = (function () { } throw new TypeError("The given parameter must be an Object or Array"); } + /** + * createIterationState - Creates the state object for searchIterator. + * @returns {Object} A new iteration state object with sane defaults. + */ function createIterationState() { return { accessors: null, @@ -266,7 +270,14 @@ var differentia = (function () { var nextTuple = {}; // Travese the Tuple's properties for (var unit in state.tuple) { - if (unit === "search" || unit === "subject" || state.accessor in state.tuple[unit]) { + if ( + ( + unit === "search" + || unit === "subject" + || isContainer(state.tuple[unit][state.accessor]) + ) + && state.accessor in state.tuple[unit] + ) { nextTuple[unit] = state.tuple[unit][state.accessor]; } } @@ -389,7 +400,7 @@ var differentia = (function () { state.tuple.compare = state.parameters.compare; }, main: function (state) { - if (!("compare" in state.tuple) && !(state.accessor in state.tuple.compare)) { + if (!("compare" in state.tuple) && !isContainer(state.tuple.compare) || !(state.accessor in state.tuple.compare)) { return true; } var subjectProp = state.currentValue; @@ -579,9 +590,9 @@ var differentia = (function () { } } }; - strategies.pathfind = { + strategies.pathFind = { interface: function (subject, findValue, search = null) { - return runStrategy(strategies.pathfind, bfs, { + return runStrategy(strategies.pathFind, bfs, { subject: subject, search: search, findValue: findValue @@ -599,6 +610,30 @@ var differentia = (function () { } } }; + strategies.diffPaths = { + interface: function (subject, compare, search = null) { + return runStrategy(strategies.diffPaths, bfs, { + subject: subject, + compare: compare, + search: search + }); + }, + entry: function (state) { + strategies.diff.entry(state); + strategies.paths.entry(state); + state.diffPaths = []; + }, + main: function (state) { + strategies.paths.main(state); + if (strategies.diff.main(state)) { + state.diffPaths[state.diffPaths.length] = Array.from(state.currentPath); + state.diffPaths[state.diffPaths.length - 1].push(state.accessor); + } + if (state.isLast) { + return state.diffPaths; + } + } + }, strategies.filter = { interface: function (subject, callback, search = null) { return runStrategy(strategies.filter, bfs, { @@ -621,10 +656,11 @@ var differentia = (function () { if (state.isLast) { while (state.pendingPaths.length > 0) { var path = state.pendingPaths.shift(); - var nodeQueue = [{ + var nodeQueue = new Queue(); + nodeQueue.push({ subject: state.subjectRoot, clone: state.cloneRoot - }]; + }); while (path.length > 0 && nodeQueue.length > 0) { var accessor = path.shift(); if (accessor === "searchRoot") { @@ -645,7 +681,7 @@ var differentia = (function () { for (var unit in tuple) { nextTuple[unit] = tuple[unit][accessor]; } - nodeQueue[nodeQueue.length] = nextTuple; + nodeQueue.push(nextTuple); } } return state.cloneRoot;