File Coverage

blib/lib/Set/Functional.pm
Criterion Covered Total %
statement 155 155 100.0
branch 34 34 100.0
condition 30 32 93.7
subroutine 31 31 100.0
pod 29 29 100.0
total 279 281 99.2


line stmt bran cond sub pod time code
1             package Set::Functional;
2              
3 5     5   87385 use 5.006;
  5         18  
  5         192  
4              
5 5     5   22 use Exporter qw{import};
  5         7  
  5         9194  
6              
7             =head1 NAME
8              
9             Set::Functional - set operations for functional programming
10              
11             =head1 VERSION
12              
13             Version 1.03
14              
15             =cut
16              
17             our $VERSION = '1.03';
18              
19             our @EXPORT_OK = qw{
20             setify setify_by
21             cartesian
22             difference difference_by
23             disjoint disjoint_by
24             distinct distinct_by
25             intersection intersection_by
26             symmetric_difference symmetric_difference_by
27             union union_by
28             is_disjoint is_disjoint_by
29             is_equal is_equal_by
30             is_equivalent is_equivalent_by
31             is_pairwise_disjoint is_pairwise_disjoint_by
32             is_proper_subset is_proper_subset_by
33             is_proper_superset is_proper_superset_by
34             is_subset is_subset_by
35             is_superset is_superset_by
36             };
37             our %EXPORT_TAGS = (all => \@EXPORT_OK);
38              
39             =head1 SYNOPSIS
40              
41             This module provides basic set operations for native lists. The primary goal
42             is to take advantage of Perl's native functional programming capabilities
43             while relying solely on Pure Perl constructs to perform the set operations as
44             fast as possible. All of these techniques have been benchmarked against other
45             common Perl idioms to determine the optimal solution. These benchmarks can
46             be found in this package (shortly).
47              
48             Each function is provided in two forms. The first form always expects simple
49             flat data structures of defined elements. The second form expects a BLOCK
50             (refered to as a choice function) to evaluate each member of the list to a
51             defined value to determine how the element is a set member. These can be
52             identified by the suffix "_by". None of these functions check definedness
53             inline so as to eliminate the costly O(n) operation. All functions have been
54             prototyped to give them a native Perl-ish look and feel.
55              
56             Example usage:
57              
58             use Set::Functional ':all';
59              
60             # Set Creation
61             my @deduped_numbers = setify(1 .. 10, 2 .. 11);
62             my @deduped_objects_by_name = setify_by { $_->{name} } ({name => 'fred'}, {name => 'bob'}, {name => 'fred'});
63              
64             # Set Operation
65             my @all_permutations = cartesian \@arr1, \@arr2, \@arr3, \@arr4;
66              
67             my @only_arr1_elements = difference \@arr1, \@arr2, \@arr3, \@arr4;
68             my @only_arr1_elements_by_name = difference_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4;
69              
70             my @unique_per_set = disjoint \@arr1, \@arr2, \@arr3, \@arr4;
71             my @unique_per_set_by_name = disjoint_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4;
72              
73             my @unique_elements = distinct \@arr1, \@arr2, \@arr3, \@arr4;
74             my @unique_elements_by_name = distinct_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4;
75              
76             my @shared_elements = intersection \@arr1, \@arr2, \@arr3, \@arr4;
77             my @shared_elements_by_name = intersection_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4;
78              
79             my @odd_occuring_elements = symmetric_difference \@arr1, \@arr2, \@arr3, \@arr4;
80             my @odd_occuring_elements_by_name = symmetric_difference_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4;
81              
82             my @all_elements = union \@arr1, \@arr2, \@arr3, \@arr4;
83             my @all_elements_by_name = union_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4;
84              
85             # Set Predicates
86             my $is_all_of_arr1_distinct_from_arr2 = is_disjoint \@arr1, \@arr2;
87             my $is_all_of_arr1_distinct_from_arr2_by_name = is_disjoint_by { $_->{name} } \@arr1, \@arr2;
88              
89             my $is_arr1_the_same_as_arr2 = is_equal \@arr1, \@arr2;
90             my $is_arr1_the_same_as_arr2_by_name = is_equal_by { $_->{name} } \@arr1, \@arr2;
91              
92             my $are_all_sets_mutually_unique = is_pairwise_disjoint \@arr1, \@arr2, \@arr3, \@arr4;
93             my $are_all_sets_mutually_unique_by_name = is_pairwise_disjoint_by { $_->{name} } \@arr1, \@arr2, \@arr3, \@arr4;
94              
95             my $is_all_of_arr1_in_arr2_but_not_the_same_as_arr2 = is_proper_subset \@arr1, \@arr2;
96             my $is_all_of_arr1_in_arr2_but_not_the_same_as_arr2_by_name = is_proper_subset_by { $_->{name} } \@arr1, \@arr2;
97              
98             my $is_all_of_arr1_in_arr2 = is_subset \@arr1, \@arr2;
99             my $is_all_of_arr1_in_arr2_by_name = is_subset_by { $_->{name} } \@arr1, \@arr2;
100              
101             my $is_all_of_arr2_in_arr1_but_not_the_same_as_arr1 = is_proper_superset \@arr1, \@arr2;
102             my $is_all_of_arr2_in_arr1_but_not_the_same_as_arr1_by_name = is_proper_superset_by { $_->{name} } \@arr1, \@arr2;
103              
104             my $is_all_of_arr2_in_arr1 = is_superset \@arr1, \@arr2;
105             my $is_all_of_arr2_in_arr1_by_name = is_superset_by { $_->{name} } \@arr1, \@arr2;
106              
107             =head1 CONSTRUCTORS
108              
109             =cut
110              
111             =head2 setify(@)
112              
113             Given a list, return a new set. Order is not guaranteed.
114              
115             setify 1 .. 10, 6 .. 15 => 1 .. 15
116              
117             =cut
118              
119             sub setify(@) {
120 4     4 1 722 my %set;
121              
122 4 100       38 undef @set{@_} if @_;
123              
124 4         34 return keys %set;
125             }
126              
127             =head2 setify_by(&@)
128              
129             Given a choice function and a list, return a new set defined by the choice
130             function. Order is not guaranteed.
131              
132             =cut
133              
134             sub setify_by(&@){
135 4     4 1 4446 my $func = shift;
136              
137 4         7 my %set;
138              
139 4 100       19 @set{ map { $func->($_) } @_ } = @_ if @_;
  41         105  
140              
141 4         78 return values %set;
142             }
143              
144             =head1 OPERATORS
145              
146             =cut
147              
148             =head2 cartesian(@)
149              
150             Given multiple set references, return multiple sets containing all permutations
151             of one element from each set. If the empty set is provided, no sets are
152             returned since the number of sets generated should be the product of the input
153             sets' cardinalities. If no sets are provided then none are returned.
154              
155             cartesian [1 .. 3], [1 .. 2] => [1,1],[1,2],[2,1],[2,2],[3,1],[3,2]
156             cartesin => ()
157             cartesin [1 .. 3], [] => ()
158              
159             =cut
160              
161             sub cartesian(@) {
162 7 100   7 1 248225 return unless @_;
163              
164 6         5 my @results;
165 6         9 my $repetitions = 1;
166              
167 6   100     39 ($repetitions *= @$_) || return for @_;
168 4         25 $#results = $repetitions - 1;
169              
170 4         9 for my $idx (0 .. $#results) {
171 2130         1468 $repetitions = @results;
172 2130         1741 $results[$idx] = [map { $_->[int($idx/($repetitions /= @$_)) % @$_] } @_];
  6610         9433  
173             }
174              
175 4         286 return @results;
176             }
177              
178             =head2 difference(@)
179              
180             Given multiple set references, return a new set with all the elements in the first set
181             that don't exist in subsequent sets.
182              
183             difference [1 .. 10], [6 .. 15] => 1 .. 5
184              
185             =cut
186              
187             sub difference(@) {
188 5     5 1 1801 my $first = shift;
189              
190 5 100 66     33 return unless $first && @$first;
191              
192 4         3 my %set;
193              
194 4         20 undef @set{@$first};
195              
196 4 100       9 do { delete @set{@$_} if @$_ } for @_;
  5         21  
197              
198 4         20 return keys %set;
199             }
200              
201             =head2 difference_by(&@)
202              
203             Given a choice function and multiple set references, return a new set with all the elements
204             in the first set that don't exist in subsequent sets according to the choice function.
205              
206             =cut
207              
208             sub difference_by(&@) {
209 5     5 1 4132 my $func = shift;
210 5         8 my $first = shift;
211              
212 5 100 66     44 return unless $first && @$first;
213              
214 4         5 my %set;
215              
216 4         10 @set{ map { $func->($_) } @$first } = @$first;
  40         89  
217              
218 4 100       38 do { delete @set{ map { $func->($_) } @$_ } if @$_ } for @_;
  5         480  
  40         80  
219              
220 4         31 return values %set;
221             }
222              
223             =head2 disjoint(@)
224              
225             Given multiple set references, return corresponding sets containing all the elements from
226             the original set that exist in any set exactly once.
227              
228             disjoint [1 .. 10], [6 .. 15] => [1 .. 5], [11 .. 15]
229              
230             =cut
231              
232             sub disjoint(@) {
233 10     10 1 3300 my %element_to_count;
234              
235 10         23 do { ++$element_to_count{$_} for @$_ } for @_;
  24         147  
236              
237 10         20 return map { [grep { $element_to_count{$_} == 1 } @$_] } @_;
  24         39  
  126         214  
238             }
239              
240             =head2 disjoint_by(&@)
241              
242             Given a choice function and multiple set references, return corresponding sets containing
243             all the elements from the original set that exist in any set exactly once
244             according to the choice function.
245              
246             =cut
247              
248             sub disjoint_by(&@) {
249 10     10 1 6157 my $func = shift;
250              
251 10         13 my %key_to_count;
252              
253 10         22 do { ++$key_to_count{$func->($_)} for @$_ } for @_;
  24         239  
254              
255 10         110 return map { [grep { $key_to_count{$func->($_)} == 1 } @$_] } @_;
  24         74  
  126         354  
256             }
257              
258             =head2 distinct(@)
259              
260             Given multiple set references, return a new set containing all the elements that exist
261             in any set exactly once.
262              
263             distinct [1 .. 10], [6 .. 15] => 1 .. 5, 11 .. 15
264              
265             =cut
266              
267             sub distinct(@) {
268 5     5 1 3732 my %element_to_count;
269              
270 5         12 do { ++$element_to_count{$_} for @$_ } for @_;
  9         64  
271              
272 5         19 return grep { $element_to_count{$_} == 1 } keys %element_to_count;
  56         86  
273             }
274              
275             =head2 distinct_by(&@)
276              
277             Given a choice function and multiple set references, return a new set containing all the
278             elements that exist in any set exactly once according to the choice function.
279              
280             =cut
281              
282             sub distinct_by(&@) {
283 5     5 1 5982 my $func = shift;
284              
285 5         7 my %key_to_count;
286              
287 5         11 for (@_) {
288 9         15 for (@$_) {
289 80         134 my $key = $func->($_);
290 80 100       349 $key_to_count{$key} = exists $key_to_count{$key} ? undef : $_;
291             }
292             }
293              
294 5         17 return grep { defined } values %key_to_count;
  56         79  
295             }
296              
297             =head2 intersection(@)
298              
299             Given multiple set references, return a new set containing all the elements that exist
300             in all sets.
301              
302             intersection [1 .. 10], [6 .. 15] => 6 .. 10
303              
304             =cut
305              
306             sub intersection(@) {
307 53     53 1 4361 my $first = shift;
308              
309 53 100 100     239 return unless $first && @$first;
310              
311 40         38 my %set;
312              
313 40         129 undef @set{@$first};
314              
315 40         63 for (@_) {
316 41         61 my @int = grep { exists $set{$_} } @$_;
  190         291  
317 41 100       91 return unless @int;
318 28         54 %set = ();
319 28         69 undef @set{@int};
320             }
321              
322 27         90 return keys %set;
323             }
324              
325             =head2 intersection_by(&@)
326              
327             Given a choice function and multiple set references, return a new set containing all the
328             elements that exist in all sets according to the choice function.
329              
330             =cut
331              
332             sub intersection_by(&@) {
333 53     53 1 5157 my $func = shift;
334 53         49 my $first = shift;
335              
336 53 100 100     239 return unless $first && @$first;
337              
338 40         43 my %set;
339              
340 40         69 @set{ map { $func->($_) } @$first } = @$first;
  220         544  
341              
342 40         305 for (@_) {
343 41         74 my @int = grep { exists $set{$func->($_)} } @$_;
  190         533  
344 41 100       190 return unless @int;
345 28         64 %set = ();
346 28         41 @set{ map { $func->($_) } @int } = @int;
  106         222  
347             }
348              
349 27         228 return values %set;
350             }
351              
352             =head2 symmetric_difference(@)
353              
354             Given multiple set references, return a new set containing all the elements that
355             exist an odd number of times across all sets.
356              
357             symmetric_difference [1 .. 10], [6 .. 15], [4, 8, 12] => 1 .. 5, 8, 11 .. 15
358              
359             =cut
360              
361             sub symmetric_difference(@) {
362 5     5 1 4159 my $count;
363             my %element_to_count;
364              
365 5         13 do { ++$element_to_count{$_} for @$_ } for @_;
  9         85  
366              
367 5         32 return grep { $element_to_count{$_} % 2 } keys %element_to_count;
  56         96  
368             }
369              
370             =head2 symmetric_difference_by(&@)
371              
372             Given a choice function and multiple set references, return a new set containing
373             all the elements that exist an odd number of times across all sets according to
374             the choice function.
375              
376             =cut
377              
378             sub symmetric_difference_by(&@) {
379 5     5 1 7524 my $func = shift;
380              
381 5         8 my $count;
382             my %key_to_count;
383              
384 5         13 do { ++$key_to_count{$func->($_)} for @$_ } for @_;
  9         203  
385              
386 80         124 return map {
387 5         105 grep {
388 9         15 $count = delete $key_to_count{$func->($_)};
389 80 100       409 defined($count) && $count % 2
390             } @$_
391             } @_;
392             }
393              
394             =head2 union(@)
395              
396             Given multiple set references, return a new set containing all the elements that exist
397             in any set.
398              
399             union [1 .. 10], [6 .. 15] => 1 .. 15
400              
401             =cut
402              
403             sub union(@) {
404 5     5 1 5650 my %set;
405              
406 5 100       13 do { undef @set{@$_} if @$_ } for @_;
  9         56  
407              
408 5         39 return keys %set;
409             }
410              
411             =head2 union_by(&@)
412              
413             Given a choice function and multiple set references, return a new set containing all the
414             elements that exist in any set according to the choice function.
415              
416             =cut
417              
418             sub union_by(&@) {
419 5     5 1 9385 my $func = shift;
420              
421 5         11 my %set;
422              
423 5 100       14 do { @set{ map { $func->($_) } @$_ } = @$_ if @$_ } for @_;
  9         83  
  80         251  
424              
425 5         72 return values %set;
426             }
427              
428             =head1 PREDICATES
429              
430             =cut
431              
432             =head2 is_disjoint($$)
433              
434             Given two set references, return true if both sets contain none of the same values.
435              
436             is_disjoint [1 .. 5], [6 .. 10] => true
437             is_disjoint [1 .. 6], [4 .. 10] => false
438              
439             =cut
440              
441             sub is_disjoint($$) {
442 8     8 1 1845 my @set = &intersection(@_[0,1]);
443 8         41 return ! @set;
444             }
445              
446             =head2 is_disjoint_by(&$$)
447              
448             Given a choice function and two sets references, return true if both sets
449             contain none of the same values according to the choice function.
450              
451             =cut
452              
453             sub is_disjoint_by(&$$) {
454 8     8 1 2622 my @set = &intersection_by(@_[0,1,2]);
455 8         31 return ! @set;
456             }
457              
458             =head2 is_equal($$)
459              
460             Given two set references, return true if both sets contain all the same values.
461             Aliased by is_equivalent.
462              
463             is_equal [1 .. 5], [1 .. 5] => true
464             is_equal [1 .. 10], [6 .. 15] => false
465              
466             =cut
467              
468             sub is_equal($$) {
469 8     8 1 1777 my @set = &intersection(@_[0,1]);
470 8   100     8 return @set == @{$_[0]} && @set == @{$_[1]};
471             }
472             *is_equivalent = \&is_equal;
473              
474             =head2 is_equal_by(&$$)
475              
476             Given a choice function and two sets references, return true if both sets
477             contain all the same values according to the choice function.
478             Aliased by is_equivalent_by.
479              
480             =cut
481              
482             sub is_equal_by(&$$) {
483 8     8 1 2701 my @set = &intersection_by(@_[0,1,2]);
484 8   100     8 return @set == @{$_[1]} && @set == @{$_[2]};
485             }
486             *is_equivalent_by = \&is_equal_by;
487              
488             =head2 is_pairwise_disjoint(@)
489              
490             Given multiple set references, return true if every set is disjoint from every
491             other set.
492              
493             is_pairwise_disjoint [1 .. 5], [6 .. 10], [11 .. 15] => true
494             is_pairwise_disjoint [1 .. 5], [6 .. 10], [11 .. 15], [3 .. 8] => false
495              
496             =cut
497              
498             sub is_pairwise_disjoint(@) {
499 5     5 1 521 my @sets = &disjoint(@_);
500 5 100       13 do { return 0 if @{$sets[$_]} != @{$_[$_]} } for 0 .. $#sets;
  12         8  
  12         15  
  12         42  
501 3         16 return 1;
502             }
503              
504             =head2 is_pairwise_disjoint_by(&@)
505              
506             Given a choice function and multiple set references, return true if every set
507             is disjoint from every other set according to the choice function.
508              
509             =cut
510              
511             sub is_pairwise_disjoint_by(&@) {
512 5     5 1 14 my @sets = &disjoint_by((shift), @_);
513 5 100       29 do { return 0 if @{$sets[$_]} != @{$_[$_]} } for 0 .. $#sets;
  12         12  
  12         17  
  12         38  
514 3         14 return 1;
515             }
516              
517             =head2 is_proper_subset($$)
518              
519             Given two set references, return true if the first set is fully contained by
520             but is not equivalent to the second.
521              
522             is_proper_subset [1 .. 5], [1 .. 10] => true
523             is_proper_subset [1 .. 5], [1 .. 5] => false
524              
525             =cut
526              
527             sub is_proper_subset($$) {
528 8     8 1 1794 my @set = &intersection(@_[0,1]);
529 8   100     8 return @set == @{$_[0]} && @set != @{$_[1]};
530             }
531              
532             =head2 is_proper_subset_by(&$$)
533              
534             Given a choice function and two set references, return true if the first set
535             is fully contained by but is not equivalent to the second according to the
536             choice function.
537              
538             =cut
539              
540             sub is_proper_subset_by(&$$) {
541 8     8 1 2606 my @set = &intersection_by(@_[0,1,2]);
542 8   100     9 return @set == @{$_[1]} && @set != @{$_[2]};
543             }
544              
545             =head2 is_proper_superset($$)
546              
547             Given two set references, return true if the first set fully contains but is
548             not equivalent to the second.
549              
550             is_proper_superset [1 .. 10], [1 .. 5] => true
551             is_proper_superset [1 .. 5], [1 .. 5] => false
552              
553             =cut
554              
555             sub is_proper_superset($$) {
556 8     8 1 1616 my @set = &intersection(@_[0,1]);
557 8   100     10 return @set != @{$_[0]} && @set == @{$_[1]};
558             }
559              
560             =head2 is_proper_superset_by(&$$)
561              
562             Given a choice function and two set references, return true if the first set
563             fully contains but is not equivalent to the second according to the choice
564             function.
565              
566             =cut
567              
568             sub is_proper_superset_by(&$$) {
569 8     8 1 2681 my @set = &intersection_by(@_[0,1,2]);
570 8   100     9 return @set != @{$_[1]} && @set == @{$_[2]};
571             }
572              
573             =head2 is_subset($$)
574              
575             Given two set references, return true if the first set is fully contained by
576             the second.
577              
578             is_subset [1 .. 5], [1 .. 10] => true
579             is_subset [1 .. 5], [1 .. 5] => true
580             is_subset [1 .. 5], [2 .. 11] => false
581              
582             =cut
583              
584             sub is_subset($$) {
585 8     8 1 1403 my @set = &intersection(@_[0,1]);
586 8         7 return @set == @{$_[0]};
  8         27  
587             }
588              
589             =head2 is_subset_by(&$$)
590              
591             Given a choice function and two set references, return true if the first set
592             is fully contained by the second according to the choice function.
593              
594             =cut
595              
596             sub is_subset_by(&$$) {
597 8     8 1 3703 my @set = &intersection_by(@_[0,1,2]);
598 8         13 return @set == @{$_[1]};
  8         42  
599             }
600              
601             =head2 is_superset($$)
602              
603             Given two set references, return true if the first set fully contains the
604             second.
605              
606             is_superset [1 .. 10], [1 .. 5] => true
607             is_superset [1 .. 5], [1 .. 5] => true
608             is_subset [1 .. 5], [2 .. 11] => false
609              
610             =cut
611              
612             sub is_superset($$) {
613 8     8 1 3279 my @set = &intersection(@_[0,1]);
614 8         7 return @set == @{$_[1]};
  8         33  
615             }
616              
617             =head2 is_superset_by(&$$)
618              
619             Given a choice function and two set references, return true if the first set
620             fully contains the second according to the choice function.
621              
622             =cut
623              
624             sub is_superset_by(&$$) {
625 8     8 1 3386 my @set = &intersection_by(@_[0,1,2]);
626 8         683 return @set == @{$_[2]};
  8         45  
627             }
628              
629             =head1 AUTHOR
630              
631             Aaron Cohen, C<< >>
632              
633             Special thanks to:
634             L
635             L
636             L
637              
638             =head1 BUGS
639              
640             Please report any bugs or feature requests to C, or through
641             the web interface at L. I will
642             be notified, and then you'll automatically be notified of progress on your bug as I make changes.
643              
644             =head1 TODO
645              
646             =over 4
647              
648             =item * Add SEE ALSO section
649              
650             =back
651              
652             =head1 SUPPORT
653              
654             You can find documentation for this module with the perldoc command.
655              
656             perldoc Set::Functional
657              
658             You can also look for information at:
659              
660             =over 4
661              
662             =item * Official GitHub Repo
663              
664             L
665              
666             =item * GitHub's Issue Tracker (report bugs here)
667              
668             L
669              
670             =item * CPAN Ratings
671              
672             L
673              
674             =item * Official CPAN Page
675              
676             L
677              
678             =back
679              
680             =head1 LICENSE AND COPYRIGHT
681              
682             Copyright 2011-2013 Aaron Cohen.
683              
684             This program is free software; you can redistribute it and/or modify it
685             under the terms of either: the GNU General Public License as published
686             by the Free Software Foundation; or the Artistic License.
687              
688             See http://dev.perl.org/licenses/ for more information.
689              
690             =cut
691              
692             1; # End of Set::Functional