0

I've created a Range class that creates a generator function for iterating over a range of integers. My next step is to create a generator function that iterates all possible permutations of values for each property. Here is the simplified code for a hard-coded example:

// Create the parameter definitions (works perfectly).
const paramDef = {
  propA: new Range(1, 2, 3), // [1,2,3] as iterator
  propB: new Range(2, 4, 6)  // [2,4,6] as iterator
};

// Hardcoded implementation (the goal is to make this generic/re-usable)
function* getUnits(def){
  // Foreach value of propA...
  for(let valPropA of def.propA.getValues()){
    // and foreach value of propB...
    for(let valPropB of def.propB.getValues()){
      // Yield an instance with the current values...
      yield {
        propA: valPropA,
        propB: valPropB
      };
    }
  }
}

// Iterate one-by-one, creating a permutation of object properties.
for(let unit of getUnits(paramDef)){
    console.log(unit);
}

// Outputs:
// {"propA":1,"propB":2}
// {"propA":1,"propB":4}
// {"propA":1,"propB":6}
// {"propA":2,"propB":2}
// {"propA":2,"propB":4}
// {"propA":2,"propB":6}
// {"propA":3,"propB":2}
// {"propA":3,"propB":4}
// {"propA":3,"propB":6}

I've tried a number of things, but the furthest I've gotten was to get the first iteration to return correctly, but nothing else. How do you generalize the getUnits() function and what traps should I look out for?

Jim Buck
  • 2,383
  • 23
  • 42
  • Is the `Outputs:` section what you hope to see, and are not, or is it what you do see, and you hope to see something else? –  Apr 06 '16 at 04:35
  • That is always the expected output. It works very easily with the provided hard-coded example (two nested loops). But I'd like to make it generic so that any object would work (regardless of number of properties). – Jim Buck Apr 06 '16 at 04:37
  • You'll want to have a look at [Javascript NodeJS ES6 permutations algorithm](http://stackoverflow.com/q/34163786/1048572) – Bergi Apr 06 '16 at 15:34

3 Answers3

1

You can use recursion over the list of property names:

function getObjectsOf(def) {
    var keys = Object.keys(def),
        o = {};
    return rec(keys.length);

    function* rec(i) {
        if (i <= 0) {
            let clone = {}; // I assume you want to yield different objects
            for (let k of keys) // or: k in o
                clone[k] = o[k];
            yield clone;
        } else {
            let key = keys[i];
            for (let value of def[key]) {
                o[key] = value;
                yield* rec(i-1);
            }
        }
    }
}

If you are looking for a more performant solution, you can dynamically generate the source of the "hardcoded" version and compile it with Function - see this answer for an example.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • @JimmyBoh: Well it depends on how you are using the generator. If you don't need two different objects at a time (and the caller doesn't mutate them), you can omit the cloning. – Bergi Apr 06 '16 at 17:09
0

How do you implement Range class? I tried this work:

class Range{
    constructor(){
        var args = Array.prototype.slice.call(arguments);
        this.getValues = function*(){
            for(const num of args){
                yield num;
            }
        };
    }
}

Using array works too:

'use strict';

const paramDef = {
  propA: [1, 2, 3], // [1,2,3] as iterator
  propB: [2, 4, 6]  // [2,4,6] as iterator
};

// Hardcoded implementation (the goal is to make this generic/re-usable)
function* getUnits(def){
  // Foreach value of propA...
  for(let valPropA of def.propA){
    // and foreach value of propB...
    for(let valPropB of def.propB){
      // Yield an instance with the current values...
      yield {
        propA: valPropA,
        propB: valPropB
      };
    }
  }
}                                                                                                                         

// Iterate one-by-one, creating a permutation of object properties.
for(let unit of getUnits(paramDef)){
    console.log(unit);
}

So, I think the problem is your Range class implementation.

hankchiutw
  • 1,546
  • 1
  • 12
  • 15
  • Thanks @Hank, but the `Range` class works perfectly fine. The `getUnit` function needs to be changed from hardcoding `propA` and `propB` to allow any number of properties. Check out my answer for my implementation of `Range`. – Jim Buck Apr 06 '16 at 16:36
0

This is the primary function (full code below):

// Generic implementation 
function* getUnits(def, props, obj){
    props = props || [];

  // If there are no remaining properties...
  if(props.length === 0){
    // Then we might be starting out...
    if(typeof obj === 'undefined'){
        // Grab the property names from the definition.
        props = Object.keys(def);
      // And create an empty object 
      obj = {};
    } else {
        yield obj;
      return;
    }
  }

  // Grab the first prop and a copy of the remaining props.
  let currentProp = props[0];
  let remainingProps = props.slice(1);
  // Foreach value of the currentProp...
  for(let val of def[currentProp].getValues()){

    // Assign the value to a new instance
    let currentObj = Object.assign({}, obj, {
        [currentProp]: val
    });
    // Pass the definition, remainingProps, and the new instance to the next level down (smaller subset of properties)
    yield* getUnits(def, remainingProps, currentObj);
  }
}

The best resource I found was MDN's Example with yield* demo. Definitly worth reading that whole article if you want a better understanding of ES6 Generators.

Credit goes out to @Bergi for indicating that the yield'ed object instance is the same in all cases, when ideally it should be cloned at each branch (so they are each different instances).

The entire example is contained in this snippet (run it to see the outcome).

// Helper class, provides an iterator from a set of args.
class Range {
  constructor() {
    this.values = Array.prototype.slice.call(arguments);
  }

  * getValues() {
    for (let i = 0; i < this.values.length; i++) {
      yield this.values[i];
    }
  }
}

// Create the parameter definitions (works perfectly).
const paramDef = {
  a: new Range(1, 2, 3),
  b: new Range(0, 1),
  c: new Range(1, 1, 2, 3, 5)
};

// Generic implementation 
function* getUnits(def, props, obj){
 props = props || [];
  
  // If there are no remaining properties...
  if(props.length === 0){
   // Then we might be starting out...
   if(typeof obj === 'undefined'){
     // Grab the property names from the definition.
     props = Object.keys(def);
      // And create an empty object 
      obj = {};
    } else {
     yield obj;
      return;
    }
  }
  
  // Grab the first prop and a copy of the remaining props.
  let currentProp = props[0];
  let remainingProps = props.slice(1);
  // Foreach value of the currentProp...
  for(let val of def[currentProp].getValues()){
   
    // Assign the value to a new instance
    let currentObj = Object.assign({}, obj, {
     [currentProp]: val
    });
    // Pass the definition, remainingProps, and the new instance to the next level down (smaller subset of properties)
    yield* getUnits(def, remainingProps, currentObj);
  }
}

let outputStr = '';

// Iterate one-by-one, creating a permutation of object properties.
for (let unit of getUnits(paramDef)) {
  outputStr += JSON.stringify(unit) + '\n';
}
alert(outputStr);

// Outputs:
// See console for the result...
Jim Buck
  • 2,383
  • 23
  • 42