Search code examples
javascriptarraysiteratorgeneratoryield

How to create a counting generator/iterator cascade in JavaScript?


Newer versions of JavaScript allow to use generators/iterators in combination with the yield keyword.


Background of my question

Consider the following generator, which "produces" numbers (digits) from 0 to 9:

// generator (produces numbers from 0 to 9, then stops)
function *zcounter() {
  var i = 0;
  while (i<=9) {
    yield i;
    i++;
  }
}

Now I want to use it to replace the following function, which uses 3 nested for loops:

// e.g.: var iArray=[0,0,0];
function IterateCascade1(iArray) {
    var iterations=0;
    for (var x=0; x<=9; x++) {
        iArray[0]=x;
        for (var y=0; y<=9; y++) {
            iArray[1]=y;
            for (var z=0; z<=9; z++) {
                iArray[2]=z;
                logArray(iArray);
                iterations++;
            }
        }
    }
    return iterations;
}

The problem

If you invoke the function above like

console.log("Iterations: "+IterateCascade1([0,0,0]));

Then it will count 1000 times from 000 to 999, which is exactly doing what I want.

The disadvantage is, that can only use arrays with 3 elements, you can't pass arrays with more elements.


To solve it by using the generator zcounter(), I tried the following:

// e.g.: var iArray=[0,0,0];
function IterateCascade2(iArray) {
    var iterations=0;
    // generate 3 iterators
    var gArray = [];
    for(var i=0; i<iArray.length; i++) {    
        var g=zcounter();
        gArray[i]=g;
    }
    // loop through
    for(var a in gArray) {
        //console.log("a:"+a);
        var g=gArray[a];
        var gnext=g.next();
        while (!gnext.done)
        {
            iArray[a]=gnext.value;
            logArray(iArray);
            gnext=g.next();
            iterations++;
        }
    }
    return iterations;
}

If you invoke the function above like

console.log("Iterations: "+IterateCascade2([0,0,0]));

Then it will count only 30 times, and it will not go through all 1000 numbers as IterateCascade1 does it.

Furthermore, if you pass larger arrays like

console.log("Iterations: "+IterateCascade2([0,0,0,0]));

then it will "count" every digit from 0 to 9, but will not go through all 10000 combinations.


Question

I know that somehow there is a recursion missing.

  • How can IterateCascade2 be modified so it does the right thing (cycle through all combinations and you can pass integer arrays of any size)?

Note:

To display the combinations, I have used

function logArray(x) {
    var result="";
    for(var i=0; i<x.length; i++) { result += x[i].toString(); }
    console.log(result);
}

in the examples above. You can use the developer tools of any of the 3 browsers or JSShell to run the code.


Solution

  • Here is a solution with recursion

    function iterareIndex(index, array, iterator) {
        var previousValue = array[index];
        for (var i = 0; i < 10; ++i) {
            array[index] = i;
            if (index == array.length - 1) {
                iterator(array);
            } else {
                iterareIndex(index + 1, array, iterator);
            }
        }
        array[index] = previousValue;
    }
    
    function IterateCascade2(array){
        iterareIndex(0, array, logArray);
    }
    

    By the way, this will give you the same output and is probably shorter:

    var size = 3;
    var prefix = '';
    for(var i = 0; i < size; ++i) {
        prefix += '0';
    }
    for(var i = 0, l = Math.pow(10, 3); i < l; ++i){
        var printableValue = (prefix+i).slice(-size);
        console.log(printableValue);
    }