File Coverage

blib/lib/Set/CrossProduct.pm
Criterion Covered Total %
statement 174 186 93.5
branch 51 70 72.8
condition 3 3 100.0
subroutine 24 25 96.0
pod 14 14 100.0
total 266 298 89.2


line stmt bran cond sub pod time code
1             package Set::CrossProduct;
2 12     12   3753354 use strict;
  12         26  
  12         479  
3              
4 12     12   61 use warnings;
  12         21  
  12         1387  
5 12     12   96 use warnings::register;
  12         25  
  12         942  
6              
7 12     12   78 use Carp qw(carp);
  12         44  
  12         1035  
8 12     12   72 use List::Util qw( reduce );
  12         26  
  12         31397  
9              
10             our $VERSION = '3.001';
11              
12             =encoding utf8
13              
14             =head1 NAME
15              
16             Set::CrossProduct - work with the cross product of two or more sets
17              
18             =head1 SYNOPSIS
19              
20             # unlabeled sets
21             my $cross = Set::CrossProduct->new( ARRAY_OF_ARRAYS );
22              
23             # or labeled sets where hash keys are the set names
24             my $cross = Set::CrossProduct->new( HASH_OF_ARRAYS );
25              
26             # get the number of tuples
27             my $number_of_tuples = $cross->cardinality;
28              
29             # get the next tuple
30             my $tuple = $cross->get;
31              
32             # move back one position
33             my $tuple = $cross->unget;
34              
35             # get the next tuple without resetting
36             # the cursor (peek at it)
37             my $next_tuple = $cross->next;
38              
39             # get the previous tuple without resetting
40             # the cursor
41             my $last_tuple = $cross->previous;
42              
43             # get a particular tuple with affecting the cursor
44             # this is zero based
45             my $nth_tuple = $cross->nth($n);
46              
47             # get a random tuple
48             my $random_tuple = $cross->random;
49              
50             # in list context returns a list of all tuples
51             my @tuples = $cross->combinations;
52              
53             # in scalar context returns an array reference to all tuples
54             my $tuples = $cross->combinations;
55              
56              
57             =head1 DESCRIPTION
58              
59             Given sets S(1), S(2), ..., S(k), each of cardinality n(1), n(2), ..., n(k)
60             respectively, the cross product of the sets is the set CP of ordered
61             tuples such that { | s1 => S(1), s2 => S(2), ....
62             sk => S(k). }
63              
64             If you do not like that description, how about:
65              
66             Create a list by taking one item from each array, and do that for all
67             possible ways that can be done, so that the first item in the list is
68             always from the first array, the second item from the second array,
69             and so on.
70              
71             If you need to see it:
72              
73             A => ( a, b, c )
74             B => ( 1, 2, 3 )
75             C => ( foo, bar )
76              
77             The cross product of A and B and C, A x B x C, is the set of
78             tuples shown:
79              
80             ( a, 1, foo )
81             ( a, 1, bar )
82             ( a, 2, foo )
83             ( a, 2, bar )
84             ( a, 3, foo )
85             ( a, 3, bar )
86             ( b, 1, foo )
87             ( b, 1, bar )
88             ( b, 2, foo )
89             ( b, 2, bar )
90             ( b, 3, foo )
91             ( b, 3, bar )
92             ( c, 1, foo )
93             ( c, 1, bar )
94             ( c, 2, foo )
95             ( c, 2, bar )
96             ( c, 3, foo )
97             ( c, 3, bar )
98              
99             In code, it looks like this:
100              
101             use v5.26;
102             use Set::CrossProduct;
103              
104             my $cross = Set::CrossProduct->new( {
105             A => [ qw( a b c ) ],
106             B => [ qw( 1 2 3 ) ],
107             C => [ qw( foo bar ) ],
108             } );
109              
110             while( my $t = $cross->get ) {
111             printf "( %s, %s, %s )\n", $t->@{qw(A B C)};
112             }
113              
114             If one of the sets happens to be empty, the cross product is empty
115             too.
116              
117             A => ( a, b, c )
118             B => ( )
119              
120             In this case, A x B is the empty set, so you'll get no tuples.
121              
122             This module combines the arrays that you give to it to create this
123             cross product, then allows you to access the elements of the
124             cross product in sequence, or to get all of the elements at
125             once. Be warned! The cardinality of the cross product, that is,
126             the number of elements in the cross product, is the product of
127             the cardinality of all of the sets.
128              
129             The constructor, C, gives you an iterator that you can
130             use to move around the cross product. You can get the next
131             tuple, peek at the previous or next tuples, or get a random
132             tuple. If you were inclined, you could even get all of the
133             tuples at once, but that might be a very large list. This module
134             lets you handle the tuples one at a time.
135              
136             I have found this module very useful for creating regression
137             tests. I identify all of the boundary conditions for all of
138             the code branches, then choose bracketing values for each of them.
139             With this module I take all of the values for each test and
140             create every possibility in the hopes of exercising all of the
141             code. Of course, your use is probably more interesting. :)
142              
143             =head2 Class Methods
144              
145             =over 4
146              
147             =item * new( [ [ ... ], [ ... ] ])
148              
149             =item * new( { LABEL => [ ... ], LABEL2 => [ ... ] } )
150              
151             Given arrays that represent some sets, return a C
152             instance that represents the cross product of those sets. If you don't
153             provide at least two sets, C returns undef and will emit a warning
154             if warnings are enabled.
155              
156             You can create the sets in two different ways: unlabeled and labeled sets.
157              
158             For unlabeled sets, you don't give them names. You rely on position. To
159             create this, pass an array of arrays:
160              
161             my $unlabeled = Set::CrossProduct->new( [
162             [ qw(1 2 3) ],
163             [ qw(a b c) ],
164             [ qw(! @ $) ],
165             ] );
166              
167             When you call C, you get an array ref where the positions in the
168             tuple correspond to the position of the sets you gave C:
169              
170             my $tuple = $unlabeled->next; # [ qw(1 a !) ]
171              
172             For labeled sets, you want to give each set a name. When you ask for a tuple,
173             you get a hash reference with the labels you choose:
174              
175             my $labeled = Set::CrossProduct->new( {
176             number => [ qw(1 2 3) ],
177             letter => [ qw(a b c) ],
178             symbol => [ qw(! @ $) ],
179             } );
180              
181             my $tuple = $labeled->next; # { number => 1, letter => 'a', symbol => '!' }
182              
183             =cut
184              
185             # The iterator object is a hash with these keys
186             #
187             # arrays - holds an array ref of array refs for each list
188             # labels - the names of the set, if applicable
189             # labeled - boolean to note if the sets are labeled or not
190             # counters - the current position in each array for generating
191             # combinations
192             # lengths - the precomputed lengths of the lists in arrays
193             # done - true if the last combination has been fetched
194             # previous - the previous value of counters in case we want
195             # to unget something and roll back the counters
196             # ungot - true if we just ungot something--to prevent
197             # attempts at multiple ungets which we don't support
198              
199             sub new {
200 18     18 1 82952 my( $class, $constructor_ref ) = @_;
201              
202 18         44 my $ref_type = ref $constructor_ref;
203              
204 18         44 my $self = bless {}, $class;
205              
206 18 100       118 if( $ref_type eq ref {} ) {
    50          
207 3         10 $self->{labeled} = 1;
208 3         26 $self->{labels} = [ sort keys %$constructor_ref ];
209 3         15 $self->{arrays} = [ @$constructor_ref{ sort keys %$constructor_ref } ];
210             }
211             elsif( $ref_type eq ref [] ) {
212 15         71 $self->{labeled} = 0;
213 15         42 $self->{arrays} = $constructor_ref;
214             }
215             else {
216 0 0       0 warnings::warn( "Set::Crossproduct->new takes an array or hash reference" ) if warnings::enabled();
217 0         0 return;
218             }
219              
220 18         89 my $array_ref = $self->{arrays};
221 18 100       73 unless( @$array_ref > 1 ) {
222 3 100       525 warnings::warn( "You need at least two sets for Set::CrossProduct to work" ) if warnings::enabled();
223 3         30 return;
224             }
225              
226 15         38 foreach my $array ( @$array_ref ) {
227 39 50       100 unless( ref $array eq ref [] ) {
228 0 0       0 warnings::warn( "Each array element or hash value needs to be an array reference" ) if warnings::enabled();
229 0         0 return;
230             }
231             }
232              
233 15         52 $self->_init;
234              
235 15         66 my $len_last = $#{ $self->{lengths} };
  15         153  
236 15         55 for( my $i = 0; $i < $#{ $self->{counters} }; $i++ ) {
  39         102  
237 24         48 my @lengths = map { $_+1 } @{ $self->{lengths} }[$i+1 .. $len_last];
  37         81  
  24         41  
238 24     13   140 $self->{factors}[$i] += reduce { $a * $b } @lengths;
  13         33  
239             }
240 15         23 push @{ $self->{factors} }, 1;
  15         30  
241              
242 15         103 return $self;
243             }
244              
245             =back
246              
247             =head2 Instance methods
248              
249             =over 4
250              
251             =cut
252              
253              
254 2     2   4 sub _factors { @{ $_[0]{factors} } }
  2         8  
255              
256             sub _increment {
257 173     173   290 my $self = shift;
258              
259             # print STDERR "_increment: counters at start: @{$self->{counters}}\n";
260             # print STDERR "_increment: previous at start: @{$self->{previous}}\n";
261 173         260 $self->{previous} = [ @{$self->{counters}} ]; # need a deep copy
  173         608  
262             # print STDERR "_increment: previous after: @{$self->{previous}}\n";
263              
264 173         276 my $tail = $#{ $self->{counters} };
  173         340  
265              
266             COUNTERS: {
267 173 100       321 if( $self->{counters}[$tail] == $self->{lengths}[$tail] ) {
  233         691  
268 66         134 $self->{counters}[$tail] = 0;
269 66         115 $tail--;
270              
271 66 100 100     264 if( $tail == 0
272             and $self->{counters}[$tail] == $self->{lengths}[$tail] ) {
273 6         41 $self->done(1);
274 6         13 return;
275             }
276              
277 60         131 redo COUNTERS;
278             }
279              
280 167         300 $self->{counters}[$tail]++;
281             }
282              
283 167         292 return 1;
284             }
285              
286             sub _init {
287 17     17   37 my( $self ) = @_;
288              
289 17         28 $self->{counters} = [ map { 0 } @{ $self->{arrays} } ];
  47         102  
  17         41  
290 17         32 $self->{lengths} = [ map { $#{$_} } @{ $self->{arrays} } ];
  47         101  
  47         96  
  17         53  
291 17         39 $self->{ungot} = 1;
292 17         26 $self->{done} = grep( $_ == -1, @{ $self->{lengths} } );
  17         62  
293              
294             # stolen from Set::CartesianProduct::Lazy by Stephen R. Scaffidi
295             # https://github.com/hercynium/Set-CartesianProduct-Lazy
296             $self->{info} = [
297             map {
298 47     49   70 [ $_, (scalar @{${ $self->{arrays} }[$_]}), reduce { $a * @$b } 1, @{ $self->{arrays} }[$_ + 1 .. $#{ $self->{arrays} }] ];
  47         76  
  47         180  
  49         215  
  47         338  
  47         85  
299 17         36 } 0 .. $#{ $self->{arrays} }
  17         44  
300             ];
301              
302 17         38 return $self;
303             }
304              
305             sub _label_tuple {
306 200     200   416 my( $self, $tuple ) = @_;
307              
308 200 100       517 unless( $self->{labeled} ) {
309 188 50       526 return wantarray ? @$tuple : $tuple;
310             }
311              
312 12         20 my %hash;
313 12         26 @hash{ @{ $self->{labels} } } = @$tuple;
  12         37  
314              
315 12 50       39 return wantarray ? %hash : \%hash;
316             }
317              
318             =item * cardinality()
319              
320             Return the carnality of the cross product. This is the number
321             of tuples, which is the product of the number of elements in
322             each set.
323              
324             Strict set theorists will realize that this isn't necessarily
325             the real cardinality since some tuples may be identical, making
326             the actual cardinality smaller.
327              
328             =cut
329              
330             sub cardinality {
331 236     236 1 32366 my $self = shift;
332              
333 236         352 my $product = 1;
334              
335 236         351 foreach my $length ( @{ $self->{lengths} } ) {
  236         546  
336 817         1318 $product *= ( $length + 1 );
337             }
338              
339 236         1317 return $product;
340             }
341              
342             =item * combinations()
343              
344             In scalar context, returns a reference to an array that contains all
345             of the tuples of the cross product. In list context, it returns the
346             list of all tuples. You should probably always use this in scalar
347             context except for very low cardinalities to avoid huge return values.
348              
349             This can be quite large, so you might want to check the cardinality
350             first. The array elements are the return values for C.
351              
352             This works by exhausting the iterator. After calling this, there will
353             be no more tuples to C. You can use C to start over.
354              
355             =cut
356              
357             sub combinations {
358 4     4 1 4834 my $self = shift;
359              
360 4         9 my @array = ();
361              
362 4         15 while( my $ref = $self->get ) {
363 78         281 push @array, $ref;
364             }
365              
366 4 50       14 if( wantarray ) { return @array }
  0         0  
367 4         16 else { return \@array }
368             }
369              
370             =item * done()
371              
372             Without an argument, C returns true if there are no more
373             combinations to fetch with C and returns false otherwise.
374              
375             With an argument, it acts as if there are no more arguments to fetch, no
376             matter the value. If you want to start over, use C instead.
377              
378             =cut
379              
380 342 100   342 1 120111 sub done { $_[0]->{done} = 1 if @_ > 1; $_[0]->{done} }
  342         1066  
381              
382             =item * get()
383              
384             Return the next tuple from the cross product, and move the position
385             to the tuple after it. If you have already gotten the last tuple in
386             the cross product, then C returns undef in scalar context and
387             the empty list in list context.
388              
389             What you get back depends on how you made the constructor.
390              
391             For unlabeled sets, you get back an array reference in scalar context
392             or a list in list context:
393              
394             For labeled sets, you get back a hash reference in scalar context or a
395             list of key-value pairs in list context.
396              
397             =cut
398              
399             sub get {
400 180     180 1 29786 my $self = shift;
401 180 100       430 return if $self->done;
402              
403 173         435 my $next_ref = $self->next;
404 173         526 $self->_increment;
405 173         351 $self->{ungot} = 0;
406              
407 173         496 $next_ref;
408             }
409              
410             =item * jump_to(N)
411              
412             (new in 3.0)
413              
414             Moves the cursor such that the next call to C will fetch tuple
415             C, which should be a positive whole number less than the cardinality.
416             Remember that everything is zero-based.
417              
418             Invalid arguments return the empty list and warn.
419              
420             This works by doing the math to reset the cursor rather than iterating
421             through the cursor to get to the right position. You can jump to any
422             position, including ones before the current cursor. After calling
423             C, C<$position> should return the value of C<$n>.
424              
425             This returns the object itself to allow you to chain methods. In previous
426             versions this returned C<1> (true). It still returns true, but just
427             a different value for it.
428              
429             =cut
430              
431             sub jump_to {
432 9     9 1 19940 my($self, $n) = @_;
433              
434 9         13 my $message = do {
435 9         18 my $guidance = 'It should be a positive whole number up to one less than the cardinality.';
436 9 100       89 if( @_ > 2 ) {
    100          
    100          
    50          
437 1         5 "too many arguments for jump_to(). $guidance";
438             }
439             elsif( ! defined $n ) {
440 2         5 "no or undefined argument for jump_to(). $guidance";
441             }
442             elsif( $n >= $self->cardinality ) {
443 4         12 sprintf "argument ($n) for jump_to() is too large for cardinality (%d). $guidance",
444             $self->cardinality;
445             }
446             elsif( $n =~ m/\D/ ) {
447 0         0 "argument ($n) for jump_to() is inappropriate. $guidance";
448             }
449             };
450 9 100       25 if( $message ) {
451 7         1088 carp $message;
452 7         80 return;
453             }
454              
455 2         7 my $max = $self->cardinality;
456 2         6 my @positions = ();
457 2         5 my $working_n = $n;
458 2         9 foreach my $factor ( $self->_factors ) {
459 8 100       19 if( $factor > $working_n ) {
460 4         6 push @positions, 0;
461 4         6 next;
462             }
463              
464 4         11 my $int = int( $working_n / $factor );
465 4         7 $working_n -= $int * $factor;
466 4         8 push @positions, $int;
467             }
468              
469 2         9 $self->{counters} = [@positions];
470              
471 2         9 $self;
472             }
473              
474             =item * labeled()
475              
476             Return true if the sets are labeled (i.e. you made the object from a
477             hash ref). Returns false otherwise.
478              
479             You might use this to figure out what sort of value C will
480             return. When the tuple is labeled, you get hash refs. Otherwise, you
481             get array refs.
482              
483             =cut
484              
485 2     2 1 5365 sub labeled { !! $_[0]->{labeled} }
486              
487             =item * next()
488              
489             Like C, but does not move the cursor. This way you can look at
490             the next tuple without affecting your position in the cross product.
491              
492             Since this does not move the cursor, repeated calls to C will
493             return the same tuple.
494              
495             =cut
496              
497             sub next {
498 185     185 1 9014 my $self = shift;
499              
500             # At end position returns undef
501 185 100       412 return unless defined $self->position;
502              
503 181         410 $self->nth( $self->position );
504             }
505              
506             =item * nth(n)
507              
508             (new in 3.0)
509              
510             Get the tuple at position C in the set (zero based). This does not
511             advance or affect the cursor. C must be a positive whole number
512             less than the cardinality. Anything else warns and returns undef.
513              
514             This was largely stolen from L by
515             Stephen R. Scaffidi.
516              
517             =cut
518              
519             # stolen from Set::CartesianProduct::Lazy by Stephen R. Scaffidi
520             # https://github.com/hercynium/Set-CartesianProduct-Lazy
521             sub nth {
522 207     207 1 25953 my($self, $n) = @_;
523              
524 207         284 my $message = do {
525 207         397 my $guidance = 'It should be a positive whole number up to one less than the cardinality.';
526 207 100       832 if( @_ > 2 ) {
    100          
    100          
    50          
527 1         2 "too many arguments for nth(). $guidance";
528             }
529             elsif( ! defined $n ) {
530 2         7 "no or undefined argument for nth(). $guidance";
531             }
532             elsif( $n >= $self->cardinality ) {
533 4         9 sprintf "argument ($n) for nth() is too large for cardinality (%d). $guidance",
534             $self->cardinality;
535             }
536             elsif( $n =~ m/\D/ ) {
537 0         0 "argument ($n) for nth() is inappropriate. $guidance";
538             }
539             };
540 207 100       557 if( $message ) {
541 7         902 carp $message;
542 7         60 return;
543             }
544              
545             my @tuple = map {
546 730         1430 my ($set_num, $set_size, $factor) = @$_;
547 730         1146 ${ $self->{arrays} }[ $set_num ][ int( $n / $factor ) % $set_size ];
  730         2521  
548 200         329 } @{ $self->{info} };
  200         480  
549              
550 200         609 my $tuple = $self->_label_tuple(\@tuple);
551              
552 200 50       633 return wantarray ? @$tuple : $tuple;
553             }
554              
555             =item * position()
556              
557             (new in 3.0)
558              
559             Returns the zero-based position of the cursor. This is the same as the
560             position for the next tuple that C will fetch. Before you fetch
561             any tuple, the position is 0. After you have fetched all the tuples,
562             C returns undef.
563              
564             =cut
565              
566             sub position {
567 536     536 1 28331 my( $self ) = $_[0];
568 536 100       1523 return if $self->{done};
569              
570 529         902 my $len_last = $#{ $self->{lengths} };
  529         1108  
571              
572 529         886 my $sum = 0;
573 529         939 for( my $i = 0; $i <= $#{ $self->{counters} }; $i++ ) {
  2529         5539  
574 2000         4079 $sum += $self->{counters}[$i] * $self->{factors}[$i];
575             }
576              
577 529         1687 return $sum;
578             }
579              
580             =item * previous()
581              
582             Like C, but does not move the cursor. This way you can look at
583             the previous tuple without affecting your position in the cross product.
584              
585             =cut
586              
587             sub previous {
588 6     6 1 7118 my $self = shift;
589              
590 6 100       22 if( $self->position == 0 ) {
591 1         214 carp "Can't call previous at the first tuple of the cross product";
592 1         10 return;
593             }
594              
595 5 50       19 $self->nth( $self->done ? $self->cardinality - 1 : $self->position - 1 );
596             }
597              
598             =item * random()
599              
600             Return a random tuple from the cross product. The return value is the
601             same as C.
602              
603             =cut
604              
605             sub random {
606 0     0 1 0 my $self = shift;
607              
608 0         0 my $rand_ref = $self->_find_ref('rand');
609              
610 0 0       0 if( wantarray ) { return (ref $rand_ref eq ref []) ? @$rand_ref : %$rand_ref }
  0 0       0  
611 0         0 else { return $rand_ref }
612             }
613              
614             =item * reset_cursor()
615              
616             Return the cursor to the first element of the cross product. The next
617             call to C will fetch the first tuple.
618              
619             This returns the object itself to allow you to chain methods. In previous
620             versions this returned C<1> (true). It still returns true, but just
621             a different value for it.
622              
623             =cut
624              
625             sub reset_cursor {
626 2     2 1 4898 my( $self, $position ) = @_;
627 2 50       9 $position = 0 unless defined $position;
628              
629 2         8 $self->_init;
630              
631 2         5 return $self;
632             }
633              
634             =item * unget()
635              
636             Pretend we did not get the tuple we just got. The next time we get a
637             tuple, we will get the same thing. You can use this to peek at the
638             next value and put it back if you do not like it.
639              
640             You can only do this for the previous tuple. C does not do
641             multiple levels of unget.
642              
643             This returns the object itself to allow you to chain methods. In previous
644             versions this returned C<1> (true). It still returns true, but just
645             a different value for it.
646              
647             =cut
648              
649             sub unget {
650 4     4 1 8554 my $self = shift;
651              
652 4 50       63 return if $self->{ungot};
653              
654 4         23 $self->{counters} = $self->{previous};
655              
656 4         10 $self->{ungot} = 1;
657              
658             # if we just got the last element, we had set the done flag,
659             # so unset it.
660 4         11 $self->{done} = 0;
661              
662 4         17 return $self;
663             }
664              
665             =back
666              
667             =head1 TO DO
668              
669             * I need to fix the cardinality method. it returns the total number
670             of possibly non-unique tuples.
671              
672             * I'd also like to do something like this:
673              
674             use Set::CrossProduct qw(setmap);
675              
676             # use setmap with an existing Set::CrossProduct object
677             my @array = setmap { ... code ... } $iterator;
678              
679             # use setmap with unnamed arrays
680             my @array = setmap { [ $_[0], $_[1] ] }
681             key => ARRAYREF, key2 => ARRAYREF;
682              
683             # use setmap with named arrays
684             my @array = setmap { [ $key1, $key2 ] }
685             key => ARRAYREF, key2 => ARRAYREF;
686              
687             # call apply() with a coderef. If the object had labels
688             # (constructed with a hash), you can use those labels in
689             # the coderef.
690             $set->apply( CODEREF );
691              
692             =head1 ISSUES
693              
694             Report an problems to L.
695              
696             =head1 SOURCE AVAILABILITY
697              
698             This source is in Github:
699              
700             http://github.com/briandfoy/set-crossproduct
701              
702             =head1 AUTHOR
703              
704             brian d foy, C<< >>
705              
706             Matt Miller implemented the named sets feature.
707              
708             Stephen R. Scaffidi implemented the code for C in his
709             L, and I adapted it for this module.
710              
711             =head1 COPYRIGHT AND LICENSE
712              
713             Copyright © 2001-2025, brian d foy . All rights reserved.
714              
715             This program is free software; you can redistribute it and/or modify
716             it under the terms of the Artistic License 2.0.
717              
718             =cut
719              
720             1;