diff --git a/common-js/_.function.permutators.js b/common-js/_.function.permutators.js new file mode 100644 index 0000000..5899893 --- /dev/null +++ b/common-js/_.function.permutators.js @@ -0,0 +1,158 @@ +module.exports = function (_) { + /** + * Lodash mixins for combinatorics + * Inspired by python itertools: https://docs.python.org/2.7/library/itertools.html + * + * Usage: + * permutations([0,1,2],2) // [[0,1],[0,2],[1,0],[1,2],[2,0],[2,1]] + * combinations([0,1,2],2) // [[0,1],[0,2],[1,2]] + * combinationsWithReplacement([0,1,2],2)// [[0,0],[0,1],[0,2],[1,1],[1,2],[2,2]] + * product([0,1,2],[0,1,2]) // [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]] + * + * Multiple input types: + * product('me','hi') + * product({who:['me','you'],say:['hi','by']}) + * product(['me','you'],['hi','by']) + * product(['me','hi']) + * combinations([0,1,2,3],2) + * permutations([1,2,3],2) + * permutations('cat',2) + */ + + + /** + * Generate all combination of arguments when given arrays or strings + * e.g. [['Ben','Jade','Darren'],['Smith','Miller']] to [['Ben','Smith'],[..]] + * e.g. 'the','cat' to [['t', 'c'],['t', 'a'], ...] + **/ + function _cartesianProductOf(args) { + if (arguments.length > 1) { args = _.toArray(arguments); } + + // strings to arrays of varters + args = _.map(args, function (opt) { return typeof opt === 'string' ? _.toArray(opt) : opt }) + + return _.reduce(args, function (a, b) { + return _.flatten(_.map(a, function (x) { + return _.map(b, function (y) { + return _.cat(x, [y]); + }); + }), false); + }, [[]]); + } + + /** Generate all combination of arguments from objects + * {Object} opts - An object or arrays with keys describing options {firstName:['Ben','Jade','Darren'],lastName:['Smith','Miller']} + * {Array} - An array of objects e.g. [{firstName:'Ben',LastName:'Smith'},{..] + **/ + function _cartesianProductObj(optObj) { + var keys = _.keys(optObj); + var opts = _.values(optObj); + var combs = _cartesianProductOf(opts); + return _.map(combs, function (comb) { + return _.zipObject(keys, comb); + }); + } + + /** + * Generate the cartesian product of input objects, arrays, or strings + * + * + * product('me','hi') + * // => [["m","h"],["m","i"],["e","h"],["e","i"]] + * + * product([1,2,3],['a','b','c'] + * // => [[1,"a"],[1,"b"],[1,"c"],[2,"a"],[2,"b"],[2,"c"],[3,"a"],[3,"b"],[3,"c"]] + * + * product({who:['me','you'],say:['hi','by']}) + * // => [{"who":"me","say":"hi"},{"who":"me","say":"by"},{"who":"you","say":"hi"},{"who":"you","say":"by"}] + * + * // It also takes in a single array of args + * product(['me','hi']) + * // => [["m","h"],["m","i"],["e","h"],["e","i"]] + */ + function product(opts) { + if (arguments.length === 1 && !_.isArray(opts)) { + return _cartesianProductObj(opts) + } + else if (arguments.length === 1) { + return _cartesianProductOf(opts) + } + else { + return _cartesianProductOf(arguments) + } + } + + /** + * Generate permutations, in all possible orderings, with no repeat values + * + * + * permutations([1,2,3],2) + * // => [[1,2],[1,3],[2,1],[2,3],[3,1],[3,2] + * + * permutations('cat',2) + * // => [["c","a"],["c","t"],["a","c"],["a","t"],["t","c"],["t","a"]] + */ + function permutations(obj, n) { + if (typeof obj == 'string') { obj = _.toArray(obj) } + n = n ? n : obj.length; + // make n copies of keys/indices + var nInds = []; + for (var j = 0; j < n; j++) { nInds.push(_.keys(obj)) } + // get product of the indices, then filter to remove the same key twice + // var arrangements = product(nInds).filter(pair=>pair[0]!==pair[1]) // this line only removes duplicates from the first two elements. + var arrangements = product(nInds); + var out = [] + for (var j = 0; j < arrangements.length; j++) { + var outt = arrangements[j].filter(function (value, index, self) { return self.indexOf(value) === index }) + if (outt.length === arrangements[j].length) { out.push(outt) } + } + return _.map(out, function (indices) { return _.map(indices, function (i) { return obj[i] }) }) + } + + + /** + * Generate n combinations of an object with no repeat values in each combination. + * + * + * combinations([0,1,2,3],2) + * // => [[0,1],[0,2],[0,3],[1,2],[1,3],[2,3]] + */ + function combinations(obj, n) { + /* filter out keys out of order, e.g. [0,1] is ok but [1,0] isn't */ + function isSorted(arr) { + return _.every(arr, function (value, index, array) { + return index === 0 || String(array[index - 1]) <= String(value); + }); + } + // array with n copies of the keys of obj + return _(permutations(_.keys(obj), n)) + .filter(isSorted) + .map(function (indices) { return _.map(indices, function (i) { return obj[i] }) }) + .value() + } + + /** + * Generate n combinations with repeat values. + * + * + * combinationsWithReplacement([0,1,2,3],2) + * // => [[0,0],[0,1],[0,2],[0,3],[1,1],[1,2],[1,3],[2,2],[2,3],[3,3]] + */ + function combinationsWithReplacement(obj, n) { + if (typeof obj == 'string') { obj = _.toArray(obj) } + n = n ? n : obj.length + // make n copies of keys/indices + for (var j = 0, nInds = []; j < n; j++) { nInds.push(_.keys(obj)) } + // get product of the indices, then filter to keep elements in order + var arrangements = product(nInds).filter(function (pair) { return pair[0] <= pair[1] }) + return _.map(arrangements, function (indices) { return _.map(indices, function (i) { return obj[i] }) }) + } + + _.mixin({ + combinations: combinations, + combinationsWithReplacement: combinationsWithReplacement, + product: product, + permutations: permutations + }) + +}; diff --git a/test/browserified.html b/test/browserified.html index ffde57c..9a9c700 100644 --- a/test/browserified.html +++ b/test/browserified.html @@ -18,6 +18,7 @@ + diff --git a/test/dist-min.html b/test/dist-min.html index 601870b..4bedace 100644 --- a/test/dist-min.html +++ b/test/dist-min.html @@ -18,6 +18,7 @@ + diff --git a/test/function.permutators.js b/test/function.permutators.js new file mode 100644 index 0000000..38aeb9a --- /dev/null +++ b/test/function.permutators.js @@ -0,0 +1,25 @@ +$(document).ready(function () { + + module("lodash.function.permutators"); + + test("product", function () { + deepEqual(_.product('me', 'hi'), [["m", "h"], ["m", "i"], ["e", "h"], ["e", "i"]], 'should return product of strings'); + deepEqual(_.product({ who: ['me', 'you'], say: ['hi', 'by'] }), [{ "who": "me", "say": "hi" }, { "who": "me", "say": "by" }, { "who": "you", "say": "hi" }, { "who": "you", "say": "by" }], 'should return product of object keys and lists'); + deepEqual(_.product(['me', 'you'], ['hi', 'by']), [["me", "hi"], ["me", "by"], ["you", "hi"], ["you", "by"]], 'should return product of lists'); + }); + + test("combinations", function () { + deepEqual(_.combinations([1, 2, 3], 2), [[1, 2], [1, 3], [2, 3]], 'should return combinations of list'); + }); + + test("combinationsWithReplacement", function () { + deepEqual(_.combinationsWithReplacement([1, 2, 3], 2), [[1, 1], [1, 2], [1, 3], [2, 2], [2, 3], [3, 3]], 'should return combinationsWithReplacement of list'); + }); + + test("permutations", function () { + deepEqual(_.permutations([1, 2, 3], 2), [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]], 'should return n=2 permutations of list'); + deepEqual(_.permutations([1, 2, 3], 3), [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]], 'should return n=3 permutations of list'); + deepEqual(_.permutations('cat', 2), [["c", "a"], ["c", "t"], ["a", "c"], ["a", "t"], ["t", "c"], ["t", "a"]], 'should return n=2 permutations of string'); + }); + +}); diff --git a/test/index.html b/test/index.html index 2ea3e8b..a41f018 100644 --- a/test/index.html +++ b/test/index.html @@ -18,6 +18,7 @@ +