File Coverage

blib/lib/Set/CrossProduct.pm
Criterion Covered Total %
statement 103 146 70.5
branch 35 62 56.4
condition 2 6 33.3
subroutine 16 21 76.1
pod 11 11 100.0
total 167 246 67.8


line stmt bran cond sub pod time code
1             package Set::CrossProduct;
2 8     8   6261 use strict;
  8         16  
  8         239  
3              
4 8     8   41 use warnings;
  8         15  
  8         199  
5 8     8   38 use warnings::register;
  8         14  
  8         13106  
6              
7             our $VERSION = '2.006';
8              
9             =encoding utf8
10              
11             =head1 NAME
12              
13             Set::CrossProduct - work with the cross product of two or more sets
14              
15             =head1 SYNOPSIS
16              
17             # unlabeled sets
18             my $iterator = Set::CrossProduct->new( ARRAY_OF_ARRAYS );
19              
20             # or labeled sets where hash keys are the set names
21             my $iterator = Set::CrossProduct->new( HASH_OF_ARRAYS );
22              
23             # get the number of tuples
24             my $number_of_tuples = $iterator->cardinality;
25              
26             # get the next tuple
27             my $tuple = $iterator->get;
28              
29             # move back one position
30             my $tuple = $iterator->unget;
31              
32             # get the next tuple without resetting
33             # the cursor (peek at it)
34             my $next_tuple = $iterator->next;
35              
36             # get the previous tuple without resetting
37             # the cursor
38             my $last_tuple = $iterator->previous;
39              
40             # get a random tuple
41             my $tuple = $iterator->random;
42              
43             # in list context returns a list of all tuples
44             my @tuples = $iterator->combinations;
45              
46             # in scalar context returns an array reference to all tuples
47             my $tuples = $iterator->combinations;
48              
49              
50             =head1 DESCRIPTION
51              
52             Given sets S(1), S(2), ..., S(k), each of cardinality n(1), n(2), ..., n(k)
53             respectively, the cross product of the sets is the set CP of ordered
54             tuples such that { | s1 => S(1), s2 => S(2), ....
55             sk => S(k). }
56              
57             If you do not like that description, how about:
58              
59             Create a list by taking one item from each array, and do that for all
60             possible ways that can be done, so that the first item in the list is
61             always from the first array, the second item from the second array,
62             and so on.
63              
64             If you need to see it:
65              
66             A => ( a, b, c )
67             B => ( 1, 2, 3 )
68             C => ( foo, bar )
69              
70             The cross product of A and B and C, A x B x C, is the set of
71             tuples shown:
72              
73             ( a, 1, foo )
74             ( a, 1, bar )
75             ( a, 2, foo )
76             ( a, 2, bar )
77             ( a, 3, foo )
78             ( a, 3, bar )
79             ( b, 1, foo )
80             ( b, 1, bar )
81             ( b, 2, foo )
82             ( b, 2, bar )
83             ( b, 3, foo )
84             ( b, 3, bar )
85             ( c, 1, foo )
86             ( c, 1, bar )
87             ( c, 2, foo )
88             ( c, 2, bar )
89             ( c, 3, foo )
90             ( c, 3, bar )
91              
92             In code, it looks like this:
93              
94             use v5.26;
95             use Set::CrossProduct;
96              
97             my $cross = Set::CrossProduct->new( {
98             A => [ qw( a b c ) ],
99             B => [ qw( 1 2 3 ) ],
100             C => [ qw( foo bar ) ],
101             } );
102              
103             while( my $t = $cross->get ) {
104             printf "( %s, %s, %s )\n", $t->@{qw(A B C)};
105             }
106              
107             If one of the sets happens to be empty, the cross product is empty
108             too.
109              
110             A => ( a, b, c )
111             B => ( )
112              
113             In this case, A x B is the empty set, so you'll get no tuples.
114              
115             This module combines the arrays that give to it to create this
116             cross product, then allows you to access the elements of the
117             cross product in sequence, or to get all of the elements at
118             once. Be warned! The cardinality of the cross product, that is,
119             the number of elements in the cross product, is the product of
120             the cardinality of all of the sets.
121              
122             The constructor, C, gives you an iterator that you can
123             use to move around the cross product. You can get the next
124             tuple, peek at the previous or next tuples, or get a random
125             tuple. If you were inclined, you could even get all of the
126             tuples at once, but that might be a very large list. This module
127             lets you handle the tuples one at a time.
128              
129             I have found this module very useful for creating regression
130             tests. I identify all of the boundary conditions for all of
131             the code branches, then choose bracketing values for each of them.
132             With this module I take all of the values for each test and
133             create every possibility in the hopes of exercising all of the
134             code. Of course, your use is probably more interesting. :)
135              
136             =head2 Class Methods
137              
138             =over 4
139              
140             =item * new( [ [ ... ], [ ... ] ])
141              
142             =item * new( { LABEL => [ ... ], LABEL2 => [ ... ] } )
143              
144             Given arrays that represent some sets, return a C
145             instance that represents the cross product of those sets. If you don't
146             provide at least two sets, C returns undef and will emit a warning
147             if warnings are enabled.
148              
149             You can create the sets in two different ways: unlabeled and labeled sets.
150              
151             For unlabeled sets, you don't give them names. You rely on position. To
152             create this, pass an array of arrays:
153              
154             my $unlabeled = Set::CrossProduct->new( [
155             [ qw(1 2 3) ],
156             [ qw(a b c) ],
157             [ qw(! @ $) ],
158             ] );
159              
160             When you call C, you get an array ref where the positions in the
161             tuple correspond to the position of the sets you gave C:
162              
163             my $tuple = $unlabeled->next; # [ qw(1 a !) ]
164              
165             For labeled sets, you want to give each set a name. When you ask for a tuple,
166             you get a hash reference with the labels you choose:
167              
168             my $labeled = Set::CrossProduct->new( {
169             number => [ qw(1 2 3) ],
170             letter => [ qw(a b c) ],
171             symbol => [ qw(! @ $) ],
172             } );
173              
174             my $tuple = $labeled->next; # { number => 1, letter => 'a', symbol => '!' }
175              
176             =cut
177              
178             # The iterator object is a hash with these keys
179             #
180             # arrays - holds an array ref of array refs for each list
181             # labels - the names of the set, if applicable
182             # labeled - boolean to note if the sets are labeled or not
183             # counters - the current position in each array for generating
184             # combinations
185             # lengths - the precomputed lengths of the lists in arrays
186             # done - true if the last combination has been fetched
187             # previous - the previous value of counters in case we want
188             # to unget something and roll back the counters
189             # ungot - true if we just ungot something--to prevent
190             # attempts at multiple ungets which we don't support
191              
192             sub new {
193 10     10 1 16272 my( $class, $constructor_ref ) = @_;
194              
195 10         25 my $ref_type = ref $constructor_ref;
196              
197 10         21 my $self = {};
198              
199 10 100       47 if( $ref_type eq ref {} ) {
    50          
200 2         6 $self->{labeled} = 1;
201 2         13 $self->{labels} = [ sort keys %$constructor_ref ];
202 2         8 $self->{arrays} = [ @$constructor_ref{ sort keys %$constructor_ref } ];
203             }
204             elsif( $ref_type eq ref [] ) {
205 8         19 $self->{labeled} = 0;
206 8         15 $self->{arrays} = $constructor_ref;
207             }
208             else {
209 0 0       0 warnings::warn( "Set::Crossproduct->new takes an array or hash reference" ) if warnings::enabled();
210 0         0 return;
211             }
212              
213 10         22 my $array_ref = $self->{arrays};
214 10 100       39 unless( @$array_ref > 1 ) {
215 3 100       464 warnings::warn( "You need at least two sets for Set::CrossProduct to work" ) if warnings::enabled();
216 3         52 return;
217             }
218              
219 7         15 foreach my $array ( @$array_ref ) {
220 13 100       53 unless( ref $array eq ref [] ) {
221 1 50       172 warnings::warn( "Each array element or hash value needs to be an array reference" ) if warnings::enabled();
222 1         31 return;
223             }
224             }
225              
226 6         13 $self->{counters} = [ map { 0 } @$array_ref ];
  12         32  
227 6         14 $self->{lengths} = [ map { $#{$_} } @$array_ref ];
  12         15  
  12         29  
228 6         11 $self->{previous} = [];
229 6         20 $self->{ungot} = 1;
230              
231 6 100       12 $self->{done} = grep( $_ == -1, @{ $self->{lengths} } )
  6         24  
232             ? 1 : 0;
233              
234 6         15 bless $self, $class;
235              
236 6         15 return $self;
237             }
238              
239             =back
240              
241             =head2 Instance methods
242              
243             =over 4
244              
245             =cut
246              
247              
248             sub _decrement {
249 0     0   0 my $self = shift;
250              
251 0         0 my $tail = $#{ $self->{counters} };
  0         0  
252              
253 0         0 $self->{counters} = $self->_previous( $self->{counters} );
254 0         0 $self->{previous} = $self->_previous( $self->{counters} );
255              
256 0         0 return 1;
257             }
258              
259             sub _find_ref {
260 27     27   51 my ($self, $which) = @_;
261              
262             my $place_func =
263 50     50   194 ($which eq 'next') ? sub { $self->{counters}[shift] }
264 4     4   16 : ($which eq 'prev') ? sub { $self->{previous}[shift] }
265 0     0   0 : ($which eq 'rand') ? sub { rand(1 + $self->{lengths}[shift]) }
266 27 0       122 : undef;
    50          
    100          
267              
268 27 50       67 return unless $place_func;
269              
270 27         43 my @indices = (0 .. $#{ $self->{arrays} });
  27         67  
271              
272 27 100       60 if ($self->{labels}) {
273 9         17 return +{ map { $self->{labels}[$_] => ${ $self->{arrays}[$_] }[ $place_func->($_) ] } @indices } }
  18         23  
  18         38  
274             else {
275 18         30 return [ map { ${ $self->{arrays}[$_] }[ $place_func->($_) ] } @indices ]
  36         44  
  36         70  
276             }
277             }
278              
279             sub _increment {
280 21     21   28 my $self = shift;
281              
282 21         32 $self->{previous} = [ @{$self->{counters}} ]; # need a deep copy
  21         47  
283              
284 21         31 my $tail = $#{ $self->{counters} };
  21         34  
285              
286             COUNTERS: {
287 21 100       42 if( $self->{counters}[$tail] == $self->{lengths}[$tail] ) {
  29         78  
288 12         32 $self->{counters}[$tail] = 0;
289 12         16 $tail--;
290              
291 12 100 66     58 if( $tail == 0
292             and $self->{counters}[$tail] == $self->{lengths}[$tail] ) {
293 4         13 $self->done(1);
294 4         8 return;
295             }
296              
297 8         15 redo COUNTERS;
298             }
299              
300 17         29 $self->{counters}[$tail]++;
301             }
302              
303 17         26 return 1;
304             }
305              
306             sub _previous {
307 0     0   0 my $self = shift;
308              
309 0         0 my $counters = $self->{counters};
310              
311 0         0 my $tail = $#{ $counters };
  0         0  
312              
313 0 0       0 return [] unless grep { $_ } @$counters;
  0         0  
314              
315             COUNTERS: {
316 0 0       0 if( $counters->[$tail] == 0 ) {
  0         0  
317 0         0 $counters->[$tail] = $self->{lengths}[$tail];
318 0         0 $tail--;
319              
320 0 0 0     0 if( $tail == 0 and $counters->[$tail] == 0) {
321 0         0 $counters = [ map { 0 } 0 .. $tail ];
  0         0  
322 0         0 last COUNTERS;
323             }
324              
325 0         0 redo COUNTERS;
326             }
327              
328 0         0 $counters->[$tail]--;
329             }
330              
331 0         0 return $counters;
332             }
333              
334             =item * cardinality()
335              
336             Return the carnality of the cross product. This is the number
337             of tuples, which is the product of the number of elements in
338             each set.
339              
340             Strict set theorists will realize that this isn't necessarily
341             the real cardinality since some tuples may be identical, making
342             the actual cardinality smaller.
343              
344             =cut
345              
346             sub cardinality {
347 4     4 1 1501 my $self = shift;
348              
349 4         9 my $product = 1;
350              
351 4         6 foreach my $length ( @{ $self->{lengths} } ) {
  4         18  
352 8         44 $product *= ( $length + 1 );
353             }
354              
355 4         58 return $product;
356             }
357              
358             =item * combinations()
359              
360             In scalar context, returns a reference to an array that contains all
361             of the tuples of the cross product. In list context, it returns the
362             list of all tuples. You should probably always use this in scalar
363             context except for very low cardinalities to avoid huge return values.
364              
365             This can be quite large, so you might want to check the cardinality
366             first. The array elements are the return values for C.
367              
368             =cut
369              
370             sub combinations {
371 2     2 1 4 my $self = shift;
372              
373 2         4 my @array = ();
374              
375 2         5 while( my $ref = $self->get ) {
376 0         0 push @array, $ref;
377             }
378              
379 2 50       5 if( wantarray ) { return @array }
  0         0  
380 2         12 else { return \@array }
381             }
382              
383             =item * done()
384              
385             Without an argument, C returns true if there are no more
386             combinations to fetch with C and returns false otherwise.
387              
388             With an argument, it acts as if there are no more arguments to fetch, no
389             matter the value. If you want to start over, use C instead.
390              
391             =cut
392              
393 36 100   36 1 98 sub done { $_[0]->{done} = 1 if @_ > 1; $_[0]->{done} }
  36         103  
394              
395             =item * get()
396              
397             Return the next tuple from the cross product, and move the position
398             to the tuple after it. If you have already gotten the last tuple in
399             the cross product, then C returns undef in scalar context and
400             the empty list in list context.
401              
402             What you get back depends on how you made the constructor.
403              
404             For unlabeled sets, you get back an array reference in scalar context
405             or a list in list context:
406              
407             For labeled sets, you get back a hash reference in scalar context or a
408             list of key-value pairs in list context.
409              
410             =cut
411              
412             sub get {
413 25     25 1 4028 my $self = shift;
414              
415 25 100       50 return if $self->done;
416              
417 21         59 my $next_ref = $self->_find_ref('next');
418              
419 21         56 $self->_increment;
420 21         33 $self->{ungot} = 0;
421              
422 21 50       36 if( wantarray ) { return (ref $next_ref eq ref []) ? @$next_ref : %$next_ref }
  7 100       31  
423 14         49 else { return $next_ref }
424             }
425              
426             =item * labeled()
427              
428             Return true if the sets are labeled (i.e. you made the object from
429             a hash ref). Returns false otherwise. You might use this to figure out
430             what sort of value C will return.
431              
432             =cut
433              
434 2     2 1 1727 sub labeled { !! $_[0]->{labeled} }
435              
436             =item * next()
437              
438             Like C, but does not move the pointer. This way you can look at
439             the next tuple without affecting your position in the cross product.
440              
441             =cut
442              
443             sub next {
444 6     6 1 1195 my $self = shift;
445              
446 6 100       15 return if $self->done;
447              
448 4         11 my $next_ref = $self->_find_ref('next');
449              
450 4 0       11 if( wantarray ) { return (ref $next_ref eq ref []) ? @$next_ref : %$next_ref }
  0 50       0  
451 4         16 else { return $next_ref }
452             }
453              
454             =item * previous()
455              
456             Like C, but does not move the pointer. This way you can look at
457             the previous tuple without affecting your position in the cross product.
458              
459             =cut
460              
461             sub previous {
462 2     2 1 558 my $self = shift;
463              
464 2         6 my $prev_ref = $self->_find_ref('prev');
465              
466 2 0       8 if( wantarray ) { return (ref $prev_ref eq ref []) ? @$prev_ref : %$prev_ref }
  0 50       0  
467 2         7 else { return $prev_ref }
468             }
469              
470             =item * random()
471              
472             Return a random tuple from the cross product. The return value is the
473             same as C.
474              
475             =cut
476              
477             sub random {
478 0     0 1 0 my $self = shift;
479              
480 0         0 my $rand_ref = $self->_find_ref('rand');
481              
482 0 0       0 if( wantarray ) { return (ref $rand_ref eq ref []) ? @$rand_ref : %$rand_ref }
  0 0       0  
483 0         0 else { return $rand_ref }
484             }
485              
486             =item * reset_cursor()
487              
488             Return the pointer to the first element of the cross product.
489              
490             =cut
491              
492             sub reset_cursor {
493 0     0 1 0 my $self = shift;
494              
495 0         0 $self->{counters} = [ map { 0 } @{ $self->{counters} } ];
  0         0  
  0         0  
496 0         0 $self->{previous} = [];
497 0         0 $self->{ungot} = 1;
498 0         0 $self->{done} = 0;
499              
500 0         0 return 1;
501             }
502              
503             =item * unget()
504              
505             Pretend we did not get the tuple we just got. The next time we get a
506             tuple, we will get the same thing. You can use this to peek at the
507             next value and put it back if you do not like it.
508              
509             You can only do this for the previous tuple. C does not do
510             multiple levels of unget.
511              
512             =cut
513              
514             sub unget {
515 3     3 1 1528 my $self = shift;
516              
517 3 50       12 return if $self->{ungot};
518              
519 3         6 $self->{counters} = $self->{previous};
520              
521 3         6 $self->{ungot} = 1;
522              
523             # if we just got the last element, we had set the done flag,
524             # so unset it.
525 3         14 $self->{done} = 0;
526              
527 3         8 return 1;
528             }
529              
530             =back
531              
532             =head1 TO DO
533              
534             * I need to fix the cardinality method. it returns the total number
535             of possibly non-unique tuples.
536              
537             * I'd also like to do something like this:
538              
539             use Set::CrossProduct qw(setmap);
540              
541             # use setmap with an existing Set::CrossProduct object
542             my @array = setmap { ... code ... } $iterator;
543              
544             # use setmap with unnamed arrays
545             my @array = setmap { [ $_[0], $_[1] ] }
546             key => ARRAYREF, key2 => ARRAYREF;
547              
548             # use setmap with named arrays
549             my @array = setmap { [ $key1, $key2 ] }
550             key => ARRAYREF, key2 => ARRAYREF;
551              
552             # call apply() with a coderef. If the object had labels
553             # (constructed with a hash), you can use those labels in
554             # the coderef.
555             $set->apply( CODEREF );
556              
557             =head1 BUGS
558              
559             * none that I know about (yet)
560              
561             =head1 SOURCE AVAILABILITY
562              
563             This source is in Github:
564              
565             http://github.com/briandfoy/set-crossproduct
566              
567             =head1 AUTHOR
568              
569             brian d foy, C<< >>
570              
571             Matt Miller implemented the named sets feature.
572              
573             =head1 COPYRIGHT AND LICENSE
574              
575             Copyright © 2001-2021, brian d foy . All rights reserved.
576              
577             This program is free software; you can redistribute it and/or modify
578             it under the terms of the Artistic License 2.0.
579              
580             =cut
581              
582             1;