diff --git a/src/core/contains.js b/src/core/contains.js index a650156..1e3d1fd 100644 --- a/src/core/contains.js +++ b/src/core/contains.js @@ -1,27 +1,41 @@ -const contains = (haystack, needle) => { - const needleType = typeof needle; - const haystackType = typeof haystack; - if (needleType !== haystackType) { - return false; - } - if (needleType === 'object') { - const needleIsArray = Array.isArray(needle); - const haystackIsArray = Array.isArray(haystack); - if (needleIsArray !== haystackIsArray) { - return false; +const objectScan = require('object-scan'); + +const scanner = objectScan(['**'], { + rtn: 'context', + abort: true, + breakFn: ({ + isLeaf, isMatch, property, value, context + }) => { + const { stack } = context; + const last = stack[stack.length - 1]; + if (isMatch && !(property in last)) { + context.result = false; + return true; } - // exact match for arrays - if (needleIsArray) { - if (needle.length !== haystack.length) { - return false; + const current = isMatch ? last[property] : last; + if (isLeaf) { + if (value !== current) { + context.result = false; + return true; } - return needle.every((e, idx) => contains(haystack[idx], e)); + } else if ( + value instanceof Object !== current instanceof Object + || Array.isArray(value) !== Array.isArray(current) + || (Array.isArray(value) && value.length !== current.length) + ) { + context.result = false; + return true; } - // subset match for object - return Object.keys(needle).every((key) => contains(haystack[key], needle[key])); + stack.push(current); + return false; + }, + filterFn: ({ context }) => { + context.stack.pop(); + return context.result !== true; } - // default comparison - return haystack === needle; -}; +}); -module.exports = contains; +module.exports = (tree, subtree) => { + const { result } = scanner(subtree, { stack: [tree], result: true }); + return result; +}; diff --git a/test/core/contains.spec.js b/test/core/contains.spec.js index 33413d4..3151be5 100644 --- a/test/core/contains.spec.js +++ b/test/core/contains.spec.js @@ -1,29 +1,93 @@ const expect = require('chai').expect; +const { describe } = require('node-tdd'); const contains = require('../../src/core/contains'); -describe('Testing contains', () => { - it('Testing String', () => { - expect(contains('value1', 'value1')).to.equal(true); - expect(contains('value1', 'value2')).to.equal(false); +describe('Testing contains', { timeout: 100000 }, () => { + it('Batch test', ({ fixture }) => { + const gen = fixture('gen'); + const containsRec = fixture('contains-rec'); + for (let x = 0; x < 10000; x += 1) { + const tree = gen(); + const subtree = gen(); + expect(contains(tree, subtree)).to.equal(containsRec(tree, subtree)); + } }); - it('Testing List', () => { - expect(contains([1, 2, 3], [1, 2, 3])).to.equal(true); - expect(contains([{ key: 'value1' }], [{ key: 'value1' }])).to.equal(true); - expect(contains([{ key: 'value1' }], [{ key: 'value2' }])).to.equal(false); - expect(contains([1, 2, 3], [3, 2, 1])).to.equal(false); - expect(contains([1, 2, 3], [1, 2])).to.equal(false); - expect(contains([], [])).to.equal(true); + describe('Testing String', () => { + it('Testing equal', () => { + expect(contains('value1', 'value1')).to.equal(true); + }); + it('Testing not equal', () => { + expect(contains('value1', 'value2')).to.equal(false); + }); }); - it('Testing Object', () => { - expect(contains({}, {})).to.equal(true); - expect(contains({ key: 'value1' }, { key: 'value1' })).to.equal(true); - expect(contains({ key: 'value1' }, { key: 'value2' })).to.equal(false); + describe('Testing Array', () => { + it('Testing equal arrays (containing three equal numbers)', () => { + expect(contains([1, 2, 3], [1, 2, 3])).to.equal(true); + }); + it('Testing not equal arrays (containing three equal numbers in different order)', () => { + expect(contains([1, 2, 3], [3, 2, 1])).to.equal(false); + }); + it('Testing equal arrays (containing single object)', () => { + expect(contains([{ key: 'value1' }], [{ key: 'value1' }])).to.equal(true); + }); + it('Testing not equal arrays (containing different single object)', () => { + expect(contains([{ key: 'value1' }], [{ key: 'value2' }])).to.equal(false); + }); + it('Testing not equal arrays (array contains number that are a subset)', () => { + expect(contains([1, 2, 3], [1, 2])).to.equal(false); + }); + it('Testing empty arrays equal', () => { + expect(contains([], [])).to.equal(true); + }); + it('Testing nested empty arrays equal', () => { + expect(contains([[], []], [[], []])).to.equal(true); + }); + it('Testing nested not equal (different arrays, removed)', () => { + expect(contains([['x'], []], [[], []])).to.equal(false); + }); + it('Testing nested not equal (different arrays, added)', () => { + expect(contains([[], []], [['x'], []])).to.equal(false); + }); }); - it('Testing Type Mismatch', () => { - expect(contains({}, '')).to.equal(false); - expect(contains({}, [])).to.equal(false); + describe('Testing Object', () => { + it('Testing empty objects equal', () => { + expect(contains({}, {})).to.equal(true); + }); + it('Testing objects equal with single key', () => { + expect(contains({ key: 'value1' }, { key: 'value1' })).to.equal(true); + }); + it('Testing objects different with same keys, but different values', () => { + expect(contains({ key: 'value1' }, { key: 'value2' })).to.equal(false); + }); + it('Testing different keys (added)', () => { + expect(contains({ key: 'value1' }, { key: 'value1', foo: 'bar' })).to.equal(false); + }); + it('Testing different keys (removed)', () => { + expect(contains({ key: 'value1', foo: 'bar' }, { key: 'value1' })).to.equal(true); + }); + }); + + describe('Testing Type Mismatch', () => { + it('Testing empty object vs empty string', () => { + expect(contains({}, '')).to.equal(false); + }); + it('Testing empty object vs empty array', () => { + expect(contains({}, [])).to.equal(false); + }); + it('Testing empty array vs empty string', () => { + expect(contains([], '')).to.equal(false); + }); + it('Testing empty array vs empty object', () => { + expect(contains([], {})).to.equal(false); + }); + it('Testing empty string vs empty object', () => { + expect(contains('', {})).to.equal(false); + }); + it('Testing empty string vs empty array', () => { + expect(contains('', [])).to.equal(false); + }); }); }); diff --git a/test/core/contains.spec.js__fixtures/contains-rec.js b/test/core/contains.spec.js__fixtures/contains-rec.js new file mode 100644 index 0000000..f389647 --- /dev/null +++ b/test/core/contains.spec.js__fixtures/contains-rec.js @@ -0,0 +1,27 @@ +const containsRec = (haystack, needle) => { + const needleType = typeof needle; + const haystackType = typeof haystack; + if (needleType !== haystackType) { + return false; + } + if (needleType === 'object') { + const needleIsArray = Array.isArray(needle); + const haystackIsArray = Array.isArray(haystack); + if (needleIsArray !== haystackIsArray) { + return false; + } + // exact match for arrays + if (needleIsArray) { + if (needle.length !== haystack.length) { + return false; + } + return needle.every((e, idx) => containsRec(haystack[idx], e)); + } + // subset match for object + return Object.keys(needle).every((key) => containsRec(haystack[key], needle[key])); + } + // default comparison + return haystack === needle; +}; + +module.exports = containsRec; diff --git a/test/core/contains.spec.js__fixtures/gen.js b/test/core/contains.spec.js__fixtures/gen.js new file mode 100644 index 0000000..58246ab --- /dev/null +++ b/test/core/contains.spec.js__fixtures/gen.js @@ -0,0 +1,18 @@ +const rand = (values) => Math.floor(Math.random() * values); + +const gen = (depth) => { + const v = rand(3); + if (v === 0 || depth >= 3) { + return rand(3); + } + if (v === 1) { + return Array.from({ length: rand(3) }, () => gen(depth + 1)); + } + const r = {}; + for (let idx = 0, len = gen(3) + 1; idx < len; idx += 1) { + r[['A', 'B', 'C'][idx]] = gen(depth + 1); + } + return r; +}; + +module.exports = () => gen(0);