Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create cartesian product (powerset?) of JavaScript objects by looping through unknown number of arrays

I'm a beginner, so please pardon my ignorance if this is trivial.

I have a javascript object of unknown length, and each property's value is an array (also of unknown length to me). For example:

var obj = {"varA":[1,2,3],
           "varB":['good','bad'],
           "varC":[0,100],
           "varD":['low','med','high']
          }

I want to loop through each of the properties and create a new object for every combination of property values. If I knew the number of properties I could just brute-forcedly use for loops, but is there a way to enumerate without knowing how many loops to hard-code?

I essentially want to do this kind of thing:

var oblist = [];
for (a in varA){
 for (b in varB){
  for (c in varC){
   for (d in varD){
    oblist.push({"varA":varA[a], "varB":varB[b], "varC":varC[c], "varD":varD[d]});
   }
  }
 }
}

so that oblist will contain objects like:

{"varA":1, "varB":"good", "varC":0, "varD":"low"}
{"varA":1, "varB":"good", "varC":0, "varD":"med"}
...
{"varA":3, "varB":"bad", "varC":100, "varD":"high"}

Thanks!

Edit: Look I'm not asking for for-loop or indexing syntax help. I'm asking what to do if I don't know the number of properties in the object (e.g. varA, varB, varC, varD, varE, hell i could have varZZ for all i know), so I can't just hard-code 4 for loops. Is there a way to set that using obj[Object.keys(obj)[i]].length?

like image 629
user4438610 Avatar asked Nov 01 '22 11:11

user4438610


2 Answers

var obj = {"varA":[1,2,3],
           "varB":['good','bad'],
           "varC":[0,100],
           "varD":['low','med','high']
          }
 
// flatten the object into an array so it's easier to work with
var obj2list = function(obj) {
  var list = [];
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      list.push({
        name: key,
        val: obj[key]
      });
    }
  }
  return list;
};
 
// implement your favorite version of clone...this isn't particular fast
var cloneObj = function(obj) {
  return JSON.parse(JSON.stringify(obj));
}
 
var iterateAndPopulateCombo = function(currentObj, listToIterate, result) {
  if (listToIterate.length == 0) {
    result.push(currentObj);
  } else {
    listToIterate[0].val.forEach(function(d) {
      var newObj = cloneObj(currentObj);
      newObj[listToIterate[0].name] = d;
      iterateAndPopulateCombo(newObj, listToIterate.slice(1), result);
    })
  }
}
 
var list = obj2list(obj);
var result = [];
iterateAndPopulateCombo({}, list, result);
console.log(JSON.stringify(result));
document.body.appendChild(document.createTextNode(JSON.stringify(result)));
like image 63
hankduan Avatar answered Nov 09 '22 07:11

hankduan


The combos you need are the cartesian product of all the arrays within your obj, here's a fiddle showing it in action:

http://jsfiddle.net/sifriday/qmyxhhny/2/

And the code...

// I think the combo you're after is known as cartesian product
// Here's a function to do it, from:
// http://stackoverflow.com/questions/12303989/cartesian-product-of-multiple-arrays-in-javascript
// It needs Underscore.js
function cartesianProductOf() {
    return _.reduce(arguments, function(a, b) {
        return _.flatten(_.map(a, function(x) {
            return _.map(b, function(y) {
                return x.concat([y]);
            });
        }), true);
    }, [ [] ]);
};

// Here's your object
var obj = {"varA":[1,2,3],
           "varB":['good','bad'],
           "varC":[0,100],
           "varD":['low','med','high']
          }

// Now I extract the arrays from your object
var idx, jdx, keys = Object.keys(obj), arrays = [], result1 = [], result2 = []
for (idx in keys) {
    var key = keys[idx]
    var arr = obj[key]
    arrays.push(arr)
}

// We can calculate the combos of the obj, but this isn't annotated. 
result1 = cartesianProductOf.apply(null, arrays)

// Now turn these back into annotated objects.
for (idx in result1) {
    var tmp = result1[idx], obj = {}
    for (jdx in tmp) {
       obj[keys[jdx]] = tmp[jdx]
    }
    result2.push(obj)
}

// Done!
console.log(result2)

With a bit of effort I think this could be tidied up; you could probably ensure the annotation happens within the cartesian product.

like image 42
sifriday Avatar answered Nov 09 '22 05:11

sifriday