14
\$\begingroup\$

I'm creating cards for a game of SET.

Can the following be rewritten in a more programmatic way, possibly getting rid of the embedded for loops?

function createCards() {
    var cards = [];
    var i, j, k, l;

    for(i = 0; i < 3; i += 1)
        for(j = 0; j < 3; j += 1)
            for(k = 0; k < 3; k += 1)
                for(l = 0; l < 3; l += 1)
                    cards.push([i, j, k, l]);

    return cards;
}

Since this is code is targetting Node, I'm happy to use EcmaScript 6 features.

\$\endgroup\$
8
  • 5
    \$\begingroup\$ What is the purpose of the code? Are those playing cards in a game? \$\endgroup\$ Jan 24, 2013 at 19:10
  • \$\begingroup\$ Yes. It's a representation for the cards in the game of Set. \$\endgroup\$
    – Randomblue
    Jan 24, 2013 at 19:11
  • \$\begingroup\$ What does the data represent? \$\endgroup\$
    – Shmiddty
    Jan 24, 2013 at 19:12
  • \$\begingroup\$ Each card has four features, and each feature has three variations. \$\endgroup\$
    – Randomblue
    Jan 24, 2013 at 19:12
  • 1
    \$\begingroup\$ Is the code doing what it's supposed to do? Because if it is, and you don't have to throw any of the cards away later, I'd say the code is as good as it's going to get. \$\endgroup\$ Jan 24, 2013 at 19:12

6 Answers 6

13
\$\begingroup\$

I'm happy to use EcmaScript 6 features

Try array comprehensions!

var vals = range(0, 3);
return [ [i, j, k, l] for (i of vals) for (j of vals) for (k of vals) for (l of vals) ]

Still, it translates to the same nested loops. If you need a variable nesting level, you can use a recursive solution.

\$\endgroup\$
4
  • \$\begingroup\$ Unfortunately Node.JS does not yet have array comprehensions. \$\endgroup\$
    – Randomblue
    Jan 24, 2013 at 23:11
  • 1
    \$\begingroup\$ Some of us who left Python were hoping to never see that kind of nightmare in our adopted homeland of Javascript…! \$\endgroup\$ May 27, 2015 at 3:41
  • \$\begingroup\$ As readers still seem to be coming to this post, it should be noted that this no longer works! stackoverflow.com/questions/31353213/… \$\endgroup\$
    – Stuart
    Nov 8, 2021 at 15:40
  • \$\begingroup\$ @Stuart Oh, this is old indeed. The modern way would be vals.flatMap(i => vals.flatMap(j => vals.flatMap(k => vals.map(l => [i, j, k, l])))) I guess \$\endgroup\$
    – Bergi
    Nov 8, 2021 at 15:49
9
\$\begingroup\$

I thought of a neat solution to this. Not sure how efficient it is, but I thought it was neat.

If you look at your result array, you'll see results from "0000" to "2222". These are the numbers from 0 to 80. In base 3!

So, using that, I whipped up a function to create all numbers in that range as an array.

function createCards(length, max){
    var cards = [],
        // How many array elements
        total = Math.pow(max, length),
        // This will help with left padding
        // since JavaScript doesn't have str_repeat('0', length)
        pad = Math.pow(10, length).toString().substring(1),
        // temp loop variables
        index, value;

    for(index = 0; index < total; index++){
        // Convert number into its new base
        value = index.toString(max);
        // Left pad it so each string is the same length
        value = pad.substring(0, pad.length - value.length) + value;
        // Push into the array
        cards.push(value.split('').map(function(a){
            // Convert result to ints
            return +a;
        }));
    }

    return cards;
}

It's called using createCards(4,3).

\$\endgroup\$
5
  • \$\begingroup\$ Great answer! While trying to come up with a solution, I went with modulo and didn't even consider base 3. \$\endgroup\$
    – Shmiddty
    Jan 24, 2013 at 19:31
  • \$\begingroup\$ @Shmiddty: I noticed his loops were making all possible 4 digit numbers with the digits 0-2. Suddenly, I realized: those are 4 digit base 3 numbers! :-) \$\endgroup\$
    – gen_Eric
    Jan 24, 2013 at 19:33
  • \$\begingroup\$ You can easily work in base-3 without creating hundreds of strings and lambda-functions... \$\endgroup\$ Jan 24, 2013 at 22:41
  • 1
    \$\begingroup\$ I like the original code more than this... \$\endgroup\$ Jan 25, 2013 at 11:13
  • \$\begingroup\$ @HonzaBrabec: It's not the best solution, but it was the 1st thing I could come up with :-P \$\endgroup\$
    – gen_Eric
    Jan 25, 2013 at 14:24
6
\$\begingroup\$

If you wanted the number and size of dimensions for your final cards array to be dynamic, you could use a recursive solution like this (DEMO):

function createCards(dimensions) {
    if (dimensions.length == 0) {
        return [];
    } else if (dimensions.length == 1) {
        var cards = [];
        for (var i = 0; i < dimensions[0]; i++)
            cards.push([i]);
        return cards;
    } else {
        var subDimensions = dimensions.splice(1);
        var subCards = createCards(subDimensions);
        var cards = [];
        for (var i = 0; i < dimensions[0]; i++)
            for (var k = 0; k < subCards.length; k++) {
                cards.push([i].concat(subCards[k]));
            }
        return cards;
    }
}

You would use it by passing in an array of dimensions:

var cards = createCards([3, 3, 3, 3]);
console.log(cards);

This solution is not the most efficient due to the re-creation of all the arrays with every dimension. However, it is very flexible and should work ok for small sets.

\$\endgroup\$
3
\$\begingroup\$

(Updated answer in 2021)

This question is asking for the Cartesian product of several arrays and so the answers to that post will work.

In the special case where the arrays are all ranges of the same length, we can write a version like this:

function repeatProduct(length, repetitions) {
    //--- Repeated Cartesian product of the array [0, 1, 2, ... length-1)]
    const arr = new Array(repetitions).fill(0);
    const r = [arr.slice()];
    for (let i = 0; i < length ** repetitions - 1; i++) {
        let j = repetitions - 1;
        while (arr[j] === length - 1) {
            arr[j] = 0;
            j--;
        }
        arr[j]++;
        r.push(arr.slice());
    }
    return r;
}
let cards = repeatedProduct(4, 4);

Or using map and flatMap, similar to this answer:

function repeatProduct(array, repetitions) {
    let r = [[]];
    while (repetitions--) {
        r = r.flatMap(sub => array.map(item => [...sub, item]));
    }
    return r;
}
let cards = repeatProduct([0, 1, 2, 3], 4);

Original answer from 2013: A flexible solution is to start with a card with all the features set to zero, then use a for loop which gets the next card in the sequence, within an outer while loop that terminates when no more valid cards can be generated. (jsfiddle).

function createCards(dim) {
    var card = [], cards = [];
    while (card.length < dim.length) card.push(0);
    while (card.length === dim.length) {
        cards.push(card.slice(0));
        for (var feature = 0; ++card[feature] === dim[feature]; feature++) {
            card[feature] = 0;
        }
    }
    return cards;
}
console.log(createCards([3, 3, 3, 3]));

Or similarly (but counting down instead of up)

function createCards(dim) {
  var cards = [], card = dim.slice(0);
  while (true) {
    cards.push(card.slice(0));
    for (var feature = dim.length - 1; !--card[feature]; card[feature] = dim[feature--]) {
      if (feature < 1) return cards;
    }
  }
}

Alternatively, loop through the current set of cards, progressively adding variations of each feature.

function createCards(dim) {
  var cards = [dim.slice(0)];
  for (var feature = 0; feature < dim.length; feature++) {
    var numberOfCards = cards.length;
    for (var variation = dim[feature] - 1; variation > 0; variation--) {
      for (var i = 0; i < numberOfCards; i++) {
        var card = cards[i].slice(0);
        card[feature] = variation;
        cards.push(card);
      }
    }
  }
  return cards;
}

These should have the same result as mellamokb's answer.

\$\endgroup\$
0
2
\$\begingroup\$

Check check it out http://jsfiddle.net/GpAUP/

Here is the meat of the code:

var maxSize = 3*3*3*3;
for(var i = 0; i < maxSize; ++i){    
    var a = Math.floor(i/27) % 3;
    var b = Math.floor(i/9) % 3;
    var c = Math.floor(i/3) % 3;
    var d = i % 3;    
    $("body").append(a + "," + b + "," + c + "," + d );
    $("body").append("<br />");
}

essentially using modulus and a little linear math. You want cards.push([a,b,c,d]) instead of appending to body. You can change the ordering if you want it a different way.

\$\endgroup\$
1
\$\begingroup\$

The code seems fine except for omitting curly braces and unfortunate naming.

I would name the features and their variations.

const numbers = [1, 2, 3];
const colors = ["Red", "Green", "Blue"];
const shapes = ["Diamond", "Ellipse", "Swirl"];
const shades = ["Full", "Striped", "Empty"];

function createDeck() {

  const deck = [];
  for(const number of numbers)
    for(const color of colors)
      for(const shapes of shapes)
        for(const shade of shades)
          deck.push({number, color, shape, shade});
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.