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   124006 use 5.006;
  5         16  
  5         194  
4              
5 5     5   27 use Exporter qw{import};
  5         8  
  5         10624  
6              
7             =head1 NAME
8              
9             Set::Functional - set operations for functional programming
10              
11             =head1 VERSION
12              
13             Version 1.0
14              
15             =cut
16              
17             our $VERSION = '1.01';
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 897 my %set;
121              
122 4 100       39 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 4209 my $func = shift;
136              
137 4         7 my %set;
138              
139 4 100       16 @set{ map { $func->($_) } @_ } = @_ if @_;
  41         125  
140              
141 4         82 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, the empty set is
152             returned. If no sets are provided then none are returned.
153              
154             cartesian [1 .. 3], [1 .. 2] => [1,1],[1,2],[2,1],[2,2],[3,1],[3,2]
155              
156             =cut
157              
158             sub cartesian(@) {
159 6 100   6 1 359148 return unless @_;
160              
161 5         10 my @results;
162 5         10 my $repetitions = 1;
163              
164 5   100     43 ($repetitions *= @$_) || return [] for @_;
165 4         35 $#results = $repetitions - 1;
166              
167 4         11 for my $idx (0 .. $#results) {
168 2130         2105 $repetitions = @results;
169 2130         2586 $results[$idx] = [map { $_->[int($idx/($repetitions /= @$_)) % @$_] } @_];
  6610         13765  
170             }
171              
172 4         427 return @results;
173             }
174              
175             =head2 difference(@)
176              
177             Given multiple set references, return a new set with all the elements in the first set
178             that don't exist in subsequent sets.
179              
180             difference [1 .. 10], [6 .. 15] => 1 .. 5
181              
182             =cut
183              
184             sub difference(@) {
185 5     5 1 1967 my $first = shift;
186              
187 5 100 66     37 return unless $first && @$first;
188              
189 4         6 my %set;
190              
191 4         25 undef @set{@$first};
192              
193 4 100       8 do { delete @set{@$_} if @$_ } for @_;
  5         26  
194              
195 4         24 return keys %set;
196             }
197              
198             =head2 difference_by(&@)
199              
200             Given a choice function and multiple set references, return a new set with all the elements
201             in the first set that don't exist in subsequent sets according to the choice function.
202              
203             =cut
204              
205             sub difference_by(&@) {
206 5     5 1 5073 my $func = shift;
207 5         6 my $first = shift;
208              
209 5 100 66     28 return unless $first && @$first;
210              
211 4         5 my %set;
212              
213 4         9 @set{ map { $func->($_) } @$first } = @$first;
  40         117  
214              
215 4 100       652 do { delete @set{ map { $func->($_) } @$_ } if @$_ } for @_;
  5         30  
  40         111  
216              
217 4         36 return values %set;
218             }
219              
220             =head2 disjoint(@)
221              
222             Given multiple set references, return corresponding sets containing all the elements from
223             the original set that exist in any set exactly once.
224              
225             disjoint [1 .. 10], [6 .. 15] => [1 .. 5], [11 .. 15]
226              
227             =cut
228              
229             sub disjoint(@) {
230 10     10 1 3819 my %element_to_count;
231              
232 10         22 do { ++$element_to_count{$_} for @$_ } for @_;
  24         144  
233              
234 10         23 return map { [grep { $element_to_count{$_} == 1 } @$_] } @_;
  24         35  
  126         291  
235             }
236              
237             =head2 disjoint_by(&@)
238              
239             Given a choice function and multiple set references, return corresponding sets containing
240             all the elements from the original set that exist in any set exactly once
241             according to the choice function.
242              
243             =cut
244              
245             sub disjoint_by(&@) {
246 10     10 1 7263 my $func = shift;
247              
248 10         13 my %key_to_count;
249              
250 10         21 do { ++$key_to_count{$func->($_)} for @$_ } for @_;
  24         325  
251              
252 10         143 return map { [grep { $key_to_count{$func->($_)} == 1 } @$_] } @_;
  24         94  
  126         458  
253             }
254              
255             =head2 distinct(@)
256              
257             Given multiple set references, return a new set containing all the elements that exist
258             in any set exactly once.
259              
260             distinct [1 .. 10], [6 .. 15] => 1 .. 5, 11 .. 15
261              
262             =cut
263              
264             sub distinct(@) {
265 5     5 1 3974 my %element_to_count;
266              
267 5         11 do { ++$element_to_count{$_} for @$_ } for @_;
  9         62  
268              
269 5         18 return grep { $element_to_count{$_} == 1 } keys %element_to_count;
  56         85  
270             }
271              
272             =head2 distinct_by(&@)
273              
274             Given a choice function and multiple set references, return a new set containing all the
275             elements that exist in any set exactly once according to the choice function.
276              
277             =cut
278              
279             sub distinct_by(&@) {
280 5     5 1 5490 my $func = shift;
281              
282 5         7 my %key_to_count;
283              
284 5         11 for (@_) {
285 9         17 for (@$_) {
286 80         143 my $key = $func->($_);
287 80 100       323 $key_to_count{$key} = exists $key_to_count{$key} ? undef : $_;
288             }
289             }
290              
291 5         14 return grep { defined } values %key_to_count;
  56         87  
292             }
293              
294             =head2 intersection(@)
295              
296             Given multiple set references, return a new set containing all the elements that exist
297             in all sets.
298              
299             intersection [1 .. 10], [6 .. 15] => 6 .. 10
300              
301             =cut
302              
303             sub intersection(@) {
304 53     53 1 3523 my $first = shift;
305              
306 53 100 100     305 return unless $first && @$first;
307              
308 40         48 my %set;
309              
310 40         169 undef @set{@$first};
311              
312 40         67 for (@_) {
313 41         70 my @int = grep { exists $set{$_} } @$_;
  190         392  
314 41 100       119 return unless @int;
315 28         65 %set = ();
316 28         106 undef @set{@int};
317             }
318              
319 27         170 return keys %set;
320             }
321              
322             =head2 intersection_by(&@)
323              
324             Given a choice function and multiple set references, return a new set containing all the
325             elements that exist in all sets according to the choice function.
326              
327             =cut
328              
329             sub intersection_by(&@) {
330 53     53 1 4350 my $func = shift;
331 53         58 my $first = shift;
332              
333 53 100 100     273 return unless $first && @$first;
334              
335 40         42 my %set;
336              
337 40         74 @set{ map { $func->($_) } @$first } = @$first;
  220         842  
338              
339 40         342 for (@_) {
340 41         79 my @int = grep { exists $set{$func->($_)} } @$_;
  190         704  
341 41 100       223 return unless @int;
342 28         74 %set = ();
343 28         111 @set{ map { $func->($_) } @int } = @int;
  106         13377  
344             }
345              
346 27         307 return values %set;
347             }
348              
349             =head2 symmetric_difference(@)
350              
351             Given multiple set references, return a new set containing all the elements that
352             exist an odd number of times across all sets.
353              
354             symmetric_difference [1 .. 10], [6 .. 15], [4, 8, 12] => 1 .. 5, 8, 11 .. 15
355              
356             =cut
357              
358             sub symmetric_difference(@) {
359 5     5 1 2888 my $count;
360             my %element_to_count;
361              
362 5         10 do { ++$element_to_count{$_} for @$_ } for @_;
  9         59  
363              
364 5         23 return grep { $element_to_count{$_} % 2 } keys %element_to_count;
  56         72  
365             }
366              
367             =head2 symmetric_difference_by(&@)
368              
369             Given a choice function and multiple set references, return a new set containing
370             all the elements that exist an odd number of times across all sets according to
371             the choice function.
372              
373             =cut
374              
375             sub symmetric_difference_by(&@) {
376 5     5 1 5712 my $func = shift;
377              
378 5         7 my $count;
379             my %key_to_count;
380              
381 5         10 do { ++$key_to_count{$func->($_)} for @$_ } for @_;
  9         157  
382              
383 80         122 return map {
384 5         96 grep {
385 9         15 $count = delete $key_to_count{$func->($_)};
386 80 100       457 defined($count) && $count % 2
387             } @$_
388             } @_;
389             }
390              
391             =head2 union(@)
392              
393             Given multiple set references, return a new set containing all the elements that exist
394             in any set.
395              
396             union [1 .. 10], [6 .. 15] => 1 .. 15
397              
398             =cut
399              
400             sub union(@) {
401 5     5 1 4458 my %set;
402              
403 5 100       13 do { undef @set{@$_} if @$_ } for @_;
  9         54  
404              
405 5         34 return keys %set;
406             }
407              
408             =head2 union_by(&@)
409              
410             Given a choice function and multiple set references, return a new set containing all the
411             elements that exist in any set according to the choice function.
412              
413             =cut
414              
415             sub union_by(&@) {
416 5     5 1 8152 my $func = shift;
417              
418 5         7 my %set;
419              
420 5 100       10 do { @set{ map { $func->($_) } @$_ } = @$_ if @$_ } for @_;
  9         85  
  80         241  
421              
422 5         66 return values %set;
423             }
424              
425             =head1 PREDICATES
426              
427             =cut
428              
429             =head2 is_disjoint($$)
430              
431             Given two set references, return true if both sets contain none of the same values.
432              
433             is_disjoint [1 .. 5], [6 .. 10] => true
434             is_disjoint [1 .. 6], [4 .. 10] => false
435              
436             =cut
437              
438             sub is_disjoint($$) {
439 8     8 1 3034 my @set = &intersection(@_[0,1]);
440 8         41 return ! @set;
441             }
442              
443             =head2 is_disjoint_by(&$$)
444              
445             Given a choice function and two sets references, return true if both sets
446             contain none of the same values according to the choice function.
447              
448             =cut
449              
450             sub is_disjoint_by(&$$) {
451 8     8 1 7940 my @set = &intersection_by(@_[0,1,2]);
452 8         44 return ! @set;
453             }
454              
455             =head2 is_equal($$)
456              
457             Given two set references, return true if both sets contain all the same values.
458             Aliased by is_equivalent.
459              
460             is_equal [1 .. 5], [1 .. 5] => true
461             is_equal [1 .. 10], [6 .. 15] => false
462              
463             =cut
464              
465             sub is_equal($$) {
466 8     8 1 2834 my @set = &intersection(@_[0,1]);
467 8   100     10 return @set == @{$_[0]} && @set == @{$_[1]};
468             }
469             *is_equivalent = \&is_equal;
470              
471             =head2 is_equal_by(&$$)
472              
473             Given a choice function and two sets references, return true if both sets
474             contain all the same values according to the choice function.
475             Aliased by is_equivalent_by.
476              
477             =cut
478              
479             sub is_equal_by(&$$) {
480 8     8 1 4032 my @set = &intersection_by(@_[0,1,2]);
481 8   100     293 return @set == @{$_[1]} && @set == @{$_[2]};
482             }
483             *is_equivalent_by = \&is_equal_by;
484              
485             =head2 is_pairwise_disjoint(@)
486              
487             Given multiple set references, return true if every set is disjoint from every
488             other set.
489              
490             is_pairwise_disjoint [1 .. 5], [6 .. 10], [11 .. 15] => true
491             is_pairwise_disjoint [1 .. 5], [6 .. 10], [11 .. 15], [3 .. 8] => false
492              
493             =cut
494              
495             sub is_pairwise_disjoint(@) {
496 5     5 1 442 my @sets = &disjoint(@_);
497 5 100       12 do { return 0 if @{$sets[$_]} != @{$_[$_]} } for 0 .. $#sets;
  12         12  
  12         16  
  12         37  
498 3         16 return 1;
499             }
500              
501             =head2 is_pairwise_disjoint_by(&@)
502              
503             Given a choice function and multiple set references, return true if every set
504             is disjoint from every other set according to the choice function.
505              
506             =cut
507              
508             sub is_pairwise_disjoint_by(&@) {
509 5     5 1 15 my @sets = &disjoint_by((shift), @_);
510 5 100       32 do { return 0 if @{$sets[$_]} != @{$_[$_]} } for 0 .. $#sets;
  12         10  
  12         18  
  12         41  
511 3         12 return 1;
512             }
513              
514             =head2 is_proper_subset($$)
515              
516             Given two set references, return true if the first set is fully contained by
517             but is not equivalent to the second.
518              
519             is_proper_subset [1 .. 5], [1 .. 10] => true
520             is_proper_subset [1 .. 5], [1 .. 5] => false
521              
522             =cut
523              
524             sub is_proper_subset($$) {
525 8     8 1 2961 my @set = &intersection(@_[0,1]);
526 8   100     13 return @set == @{$_[0]} && @set != @{$_[1]};
527             }
528              
529             =head2 is_proper_subset_by(&$$)
530              
531             Given a choice function and two set references, return true if the first set
532             is fully contained by but is not equivalent to the second according to the
533             choice function.
534              
535             =cut
536              
537             sub is_proper_subset_by(&$$) {
538 8     8 1 3242 my @set = &intersection_by(@_[0,1,2]);
539 8   100     10 return @set == @{$_[1]} && @set != @{$_[2]};
540             }
541              
542             =head2 is_proper_superset($$)
543              
544             Given two set references, return true if the first set fully contains but is
545             not equivalent to the second.
546              
547             is_proper_superset [1 .. 10], [1 .. 5] => true
548             is_proper_superset [1 .. 5], [1 .. 5] => false
549              
550             =cut
551              
552             sub is_proper_superset($$) {
553 8     8 1 2929 my @set = &intersection(@_[0,1]);
554 8   100     9 return @set != @{$_[0]} && @set == @{$_[1]};
555             }
556              
557             =head2 is_proper_superset_by(&$$)
558              
559             Given a choice function and two set references, return true if the first set
560             fully contains but is not equivalent to the second according to the choice
561             function.
562              
563             =cut
564              
565             sub is_proper_superset_by(&$$) {
566 8     8 1 3235 my @set = &intersection_by(@_[0,1,2]);
567 8   100     10 return @set != @{$_[1]} && @set == @{$_[2]};
568             }
569              
570             =head2 is_subset($$)
571              
572             Given two set references, return true if the first set is fully contained by
573             the second.
574              
575             is_subset [1 .. 5], [1 .. 10] => true
576             is_subset [1 .. 5], [1 .. 5] => true
577             is_subset [1 .. 5], [2 .. 11] => false
578              
579             =cut
580              
581             sub is_subset($$) {
582 8     8 1 3898 my @set = &intersection(@_[0,1]);
583 8         13 return @set == @{$_[0]};
  8         49  
584             }
585              
586             =head2 is_subset_by(&$$)
587              
588             Given a choice function and two set references, return true if the first set
589             is fully contained by the second according to the choice function.
590              
591             =cut
592              
593             sub is_subset_by(&$$) {
594 8     8 1 3238 my @set = &intersection_by(@_[0,1,2]);
595 8         12 return @set == @{$_[1]};
  8         37  
596             }
597              
598             =head2 is_superset($$)
599              
600             Given two set references, return true if the first set fully contains the
601             second.
602              
603             is_superset [1 .. 10], [1 .. 5] => true
604             is_superset [1 .. 5], [1 .. 5] => true
605             is_subset [1 .. 5], [2 .. 11] => false
606              
607             =cut
608              
609             sub is_superset($$) {
610 8     8 1 4595 my @set = &intersection(@_[0,1]);
611 8         107 return @set == @{$_[1]};
  8         60  
612             }
613              
614             =head2 is_superset_by(&$$)
615              
616             Given a choice function and two set references, return true if the first set
617             fully contains the second according to the choice function.
618              
619             =cut
620              
621             sub is_superset_by(&$$) {
622 8     8 1 3210 my @set = &intersection_by(@_[0,1,2]);
623 8         11 return @set == @{$_[2]};
  8         52  
624             }
625              
626             =head1 AUTHOR
627              
628             Aaron Cohen, C<< >>
629              
630             Special thanks to:
631             L
632             L
633              
634             =head1 BUGS
635              
636             Please report any bugs or feature requests to C, or through
637             the web interface at L. I will
638             be notified, and then you'll automatically be notified of progress on your bug as I make changes.
639              
640             =head1 TODO
641              
642             =over 4
643              
644             =item * Add SEE ALSO section
645              
646             =back
647              
648             =head1 SUPPORT
649              
650             You can find documentation for this module with the perldoc command.
651              
652             perldoc Set::Functional
653              
654             You can also look for information at:
655              
656             =over 4
657              
658             =item * Official GitHub Repo
659              
660             L
661              
662             =item * GitHub's Issue Tracker (report bugs here)
663              
664             L
665              
666             =item * CPAN Ratings
667              
668             L
669              
670             =item * Official CPAN Page
671              
672             L
673              
674             =back
675              
676             =head1 LICENSE AND COPYRIGHT
677              
678             Copyright 2011-2013 Aaron Cohen.
679              
680             This program is free software; you can redistribute it and/or modify it
681             under the terms of either: the GNU General Public License as published
682             by the Free Software Foundation; or the Artistic License.
683              
684             See http://dev.perl.org/licenses/ for more information.
685              
686             =cut
687              
688             1; # End of Set::Functional