skeetastax
skeetastax

Reputation: 1658

How do I get all possible combinations (not permutations) from two different arrays in Perl?

Say I have these two arrays:

my @varA = ("a","b"); # all possible values of varA
my @varB = (1,2); # all possible values of varB

and I want to get the output variable combinations:

varA = a, varB = 1; # the first possible combination
varA = a, varB = 2;
varA = b, varB = 1;
varA = b, varB = 2; # the last possible combination

I looked into the Algorithm::Combinatorics and related modules, but I can only see examples where they are used to get combinations of elements from a single array.

Is there a simpler way to do this than nested loops?

Edit: For the sake of completeness, I have a number of keys, named variables, that can take on one of a number of possible values. I need to generate all combinations of each possible key=value pair with each other (not repeating the same key - each combination only has unique keys).

Let's say I have the following attribute keys:

ShoeSize # can be one of a set of size values
CurrentAge # can be any integer from 0 up to, say, 110 years
HeightInCm # can be any integer up to, say, 200 (2m tall!)

Now let's say I am studying statistics and I want to generate every combination of ShoeSize, HeightInCm and CurrentAge, then start counting how many people match each combination of values (ignore the counting bit, though - that is just for this illustration - there will only be one instance of each of my real combinations).

I am storing each Combination as a '3-key hash' within an array (the references, that is, each pointing to an instance of the same anonymous hash set).

Example Output:

@AoH = (
  [ # element 0
    {
      ShoeSize=10,
      CurrentAge=14,
      HeightInCm=150
    }
  ],
  [ # element 1
    {
      ShoeSize=12,
      CurrentAge=23,
      HeightInCm=172
    }
  ],
  [ # element 2
    {
      ShoeSize=8,
      CurrentAge=64,
      HeightInCm=167
    }
  ],
  [ # element 3
    {
      etc.
    }
  ]
)

How to generate this array of combination hashes with the correct set of unique keys is the real question.

Upvotes: 1

Views: 475

Answers (2)

zdim
zdim

Reputation: 66883

This is a Cartesian product you seem to be asking about.

There are libraries for that, like Set::CrossProduct and Math::Cartesian::Product. The first one provides a generator, that can be used in a variety of ways, the second one produces the whole result list at once, which can be filtered.

It is not clear to me how you'd like to combine the elements from two sets (lists) -- concatenate values? form tuples? strings? Please clarify and I can add specific examples, if needed.

Also note that in principle that can be just a double map or such

my @cp = map { my $e = $_; map { $_ . $e } @ary2 } @ary1;

(here the elements are concatenated)


Edit   A complete example has been added to the question, clarifying it. Here is one way for it, using a module for a cross product of three arrays (a triple-map would be a little unsightly).

The desired output described in the question is built inside the code block available for that.

use warnings;
use strict;
use feature 'say';
use Data::Dump qw(dd);

use List::Gen qw(cartesian);

my @shoe_size = (8,10);     # (apologies to all "other" people...)
my @curr_age  = (21, 40);
my @height_cm = (170, 200);

# Returns a generator
my $gen = cartesian { 
    { shoe_size => $_[0], curr_age => $_[1], height_cm => $_[2] } 
} 
\@shoe_size, \@curr_age, \@height_cm;
    
my @combs = @$gen;  # Now this is an array of hashrefs

dd \@combs;

One can use any other method to form the cartesian product instead, and built that hashref out of each combination.

The code above prints

[
  { curr_age => 21, height_cm => 170, shoe_size => 8 },
  { curr_age => 21, height_cm => 200, shoe_size => 8 },
  { curr_age => 40, height_cm => 170, shoe_size => 8 },
  { curr_age => 40, height_cm => 200, shoe_size => 8 },
  { curr_age => 21, height_cm => 170, shoe_size => 10 },
  { curr_age => 21, height_cm => 200, shoe_size => 10 },
  { curr_age => 40, height_cm => 170, shoe_size => 10 },
  { curr_age => 40, height_cm => 200, shoe_size => 10 },
]

I place hashrefs directly in one array, but if desired output indeed needs each to be a sole element of an arrayref, as shown in the question, that is easy to modify.

The order of hash(ref) elements in printing can be sorted as desired. For simplicity I use plain arrays for each data set and then hard-code their names in the output; this can be avoided by arranging data suitably.

One clear option is to use a hash for all data, so that each set has a label, for which we can then provide a sorting order. This has another benefit: we can then list the names in their desired order at one place in the program. Let me know if an example would be useful.


There is more out there. For one, if you can anyway make use of higher-order tools then definitely check out the mesmerizing List::Gen, which also provides a cartesian function. The module hasn't been touched in a decade but it's all pure Perl so you can use its source for things of interest.


Examples using other tools to build the product

With Set::CrossProduct

use Data::Dump qw(dd);

use Set::CrossProduct; 
    
my $cp = Set::CrossProduct->new( { # can give them names
    shoe_size => \@shoe_size,
    curr_age  => \@curr_age,
    height_cm => \@height_cm
} );

# Generator ($cp object) is ready

my $combs = $cp->combinations;  # all combinations at once

dd $combs;

The generator can be used to iterate over combinations one at a time as well. It has a small and nicely chosen set of functionalities for its generator.

With Math::Cartesian::Product

use Data::Dump qw(dd);

use Math::Cartesian::Product;

my @cp = map { 
        { shoe_size => $_->[0], curr_age => $_->[1], height_cm => $_->[2] } 
    }
    cartesian { 1 } \@shoe_size, \@curr_age, \@height_cm;

dd $_ for @cp;

The function cartesian returns an array with arrayrefs, each with elements in a combination, of all combinations for which the block {...} returns true. (The block is just a filter, but is compulsory in the syntax.)

Then that is passed through a map to format for the desired output.

Upvotes: 5

ikegami
ikegami

Reputation: 385829

use Algorithm::Loops qw( NestedLoops );

my $iter = NestedLoops([ \@varA, \@varB ]);
while ( my ($varA, $varB) = $iter->() ) {
   say "varA = $varA, varB = $varB;";
}

or

use Algorithm::Loops qw( NestedLoops );

NestedLoops(
   [ \@varA, \@varB ],
   sub {
      my ($varA, $varB) = @_;
      say "varA = $varA, varB = $varB;";
   }
);

Upvotes: 1

Related Questions