diff --git a/libloki/service_nodes.js b/libloki/service_nodes.js new file mode 100644 index 000000000..113d97c6a --- /dev/null +++ b/libloki/service_nodes.js @@ -0,0 +1,29 @@ +function consolidateLists(lists, threshold = 1){ + if (typeof threshold !== 'number') { + throw Error('Provided threshold is not a number'); + } + + // calculate list size manually since `Set` + // does not have a `length` attribute + let listSize = 0; + const occurences = {}; + lists.forEach(list => { + listSize += 1; + list.forEach(item => { + if (!(item in occurences)) { + occurences[item] = 1; + } else { + occurences[item] += 1; + } + }); + }); + + const scaledThreshold = listSize * threshold; + return Object.entries(occurences) + .filter(keyValue => keyValue[1] >= scaledThreshold) + .map(keyValue => keyValue[0]); +} + +module.exports = { + consolidateLists, +} diff --git a/libloki/test/node/.eslintrc.js b/libloki/test/node/.eslintrc.js new file mode 100644 index 000000000..904f317ff --- /dev/null +++ b/libloki/test/node/.eslintrc.js @@ -0,0 +1,22 @@ +// For reference: https://github.com/airbnb/javascript + +module.exports = { + env: { + mocha: true, + browser: false, + }, + + parserOptions: { + sourceType: 'module', + }, + + rules: { + // We still get the value of this rule, it just allows for dev deps + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + }, + ], + }, +}; diff --git a/libloki/test/node/service_nodes_test.js b/libloki/test/node/service_nodes_test.js new file mode 100644 index 000000000..9433ee934 --- /dev/null +++ b/libloki/test/node/service_nodes_test.js @@ -0,0 +1,58 @@ +const ServiceNode = require('../../service_nodes'); +const { expect } = require('chai'); + +describe('ServiceNodes', () => { + describe('#consolidateLists', () => { + it('should throw when provided a non-iterable list', () => { + expect(() => ServiceNode.consolidateLists(null, 1)).to.throw(); + }); + it('should throw when provided a non-iterable item in the list', () => { + expect(() => ServiceNode.consolidateLists([1, 2, 3], 1)).to.throw(); + }); + it('should throw when provided a non-number threshold', () => { + expect(() => ServiceNode.consolidateLists([], 'a')).to.throw(); + }); + it('should return an empty array when the input is an empty array', () => { + const result = ServiceNode.consolidateLists([]); + expect(result).to.deep.equal([]); + }); + it('should return the input when only 1 list is provided', () => { + const result = ServiceNode.consolidateLists([['a', 'b', 'c']]); + expect(result).to.deep.equal(['a', 'b', 'c']); + }); + it('should return the union of all lists when threshold is 0', () => { + const result = ServiceNode.consolidateLists([ + ['a', 'b', 'c', 'h'], + ['d', 'e', 'f', 'g'], + ['g', 'h'], + ], 0); + expect(result.sort()).to.deep.equal(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); + }); + it('should return the intersection of all lists when threshold is 1', () => { + const result = ServiceNode.consolidateLists([ + ['a', 'b', 'c', 'd'], + ['a', 'e', 'f', 'g'], + ['a', 'h'], + ], 1); + expect(result).to.deep.equal(['a']); + }); + it('should return the elements that have an occurence >= the provided threshold', () => { + const result = ServiceNode.consolidateLists([ + ['a', 'b', 'c', 'd', 'e', 'f', 'g'], + ['a', 'b', 'c', 'd', 'e', 'f', 'h'], + ['a', 'b', 'c', 'd', 'e', 'f', 'g'], + ['a', 'b', 'c', 'd', 'e', 'g', 'h'], + ], 3/4); + expect(result).to.deep.equal(['a', 'b', 'c', 'd', 'e', 'f', 'g']); + }); + it('should work with sets as well', () => { + const result = ServiceNode.consolidateLists(new Set([ + new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']), + new Set(['a', 'b', 'c', 'd', 'e', 'f', 'h']), + new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']), + new Set(['a', 'b', 'c', 'd', 'e', 'g', 'h']), + ]), 3/4); + expect(result).to.deep.equal(['a', 'b', 'c', 'd', 'e', 'f', 'g']); + }); + }); +}); diff --git a/package.json b/package.json index 21dfe6bd7..b1b3e914b 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "test-lib-view": "NODE_ENV=test-lib yarn run start", "test-loki-view": "NODE_ENV=test-loki yarn run start", "test-electron": "yarn grunt test", - "test-node": "mocha --recursive test/app test/modules ts/test", + "test-node": "mocha --recursive test/app test/modules ts/test libloki/test/node", "test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test", "eslint": "eslint .", "lint": "yarn format --list-different && yarn lint-windows",