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 @@
+