File Coverage

blib/lib/Sort/MultipleFields.pm
Criterion Covered Total %
statement 43 43 100.0
branch 17 20 85.0
condition n/a
subroutine 14 14 100.0
pod 2 2 100.0
total 76 79 96.2


line stmt bran cond sub pod time code
1             # $Id: MultipleFields.pm,v 1.9 2008/07/25 18:57:32 drhyde Exp $
2              
3             package Sort::MultipleFields;
4              
5 1     1   22866 use strict;
  1         2  
  1         38  
6 1     1   5 use warnings;
  1         1  
  1         43  
7              
8 1     1   4 use vars qw($VERSION @EXPORT_OK @ISA);
  1         6  
  1         86  
9              
10 1     1   6 use Scalar::Util qw(reftype);
  1         1  
  1         142  
11              
12 1     1   11 use Exporter; # 5.6's Exporter doesn't export its import function, so
  1         2  
  1         705  
13             # need to do the inheritance dance. Joy.
14             @ISA = qw(Exporter);
15             @EXPORT_OK = qw(mfsort mfsortmaker);
16              
17             $VERSION = '0.001_01';
18              
19             =head1 NAME
20              
21             Sort::MultipleFields - Conveniently sort on multiple fields
22              
23             =head1 SYNOPSIS
24              
25             use Sort::MultipleFields qw(mfsort);
26              
27             my $library = mfsort {
28             author => 'ascending',
29             title => 'ascending'
30             } (
31             {
32             author => 'Hoyle, Fred',
33             title => 'Black Cloud, The'
34             },
35             {
36             author => 'Clarke, Arthur C',
37             title => 'Rendezvous with Rama'
38             },
39             {
40             author => 'Clarke, Arthur C',
41             title => 'Islands In The Sky'
42             }
43             );
44              
45             after which C<$library> would be a reference to a list of three hashrefs,
46             which would be (in order) the data for "Islands In The Sky", "Rendezvous
47             with Rama", and "The Black Cloud".
48              
49             =head1 DESCRIPTION
50              
51             This provides a simple way of sorting structured data with multiple fields.
52             For instance, you might want to sort a list of books first by author and
53             within each author sort by title.
54              
55             =head1 EXPORTS
56              
57             The subroutines may be exported if you wish, but are not exported by
58             default.
59              
60             Default-export is bad and wrong and people who do it should be spanked.
61              
62             =head1 SUBROUTINES
63              
64             =head2 mfsort
65              
66             @sorted = mfsort { SORT SPEC } @unsorted;
67              
68             Takes a sort specification and a list (or list-ref) of references to hashes.
69             It returns either a list or a list-ref, depending on context.
70              
71             The sort specification is a block structured thus:
72              
73             {
74             field1 => 'ascending',
75             field2 => 'descending',
76             field3 => sub {
77             lc($_[0]) cmp lc($_[1]) # case-insensitive ascending
78             },
79             ...
80             }
81              
82             Yes, it looks like a hash. But it's not, it's a block that returns a
83             list, and order matters.
84              
85             The spec is a list of pairs, each consisting of a field to sort on, and
86             how to sort it. How to sort is simply a function that, when given a
87             pair of pieces of data, will return -1, 0 or 1 depending on whether the first
88             argument is "less than", equal to, or "greater than" the second argument.
89             Sounds familiar, doesn't it. As short-cuts for the most common sorts,
90             the following case-insensitive strings will work:
91              
92             =over
93              
94             =item ascending, or asc
95              
96             Sort ASCIIbetically, ascending (ie C<$a cmp $b>)
97              
98             =item descending, or desc
99              
100             Sort ASCIIbetically, descending (ie C<$b cmp $a>)
101              
102             =item numascending, or numasc
103              
104             Sort numerically, ascending (ie C<$a <=> $b>)
105              
106             =item numdescending, or numdesc
107              
108             Sort numerically, descending (ie C<$b <=> $a>)
109              
110             =back
111              
112             Really old versions
113             of perl might require that you instead pass the sort spec as an
114             anonymous subroutine.
115              
116             mfsort sub { ... }, @list
117              
118             =cut
119              
120             sub mfsort(&@) {
121 24     24 1 7079 my $spec = shift;
122 24         42 my @records = @_;
123 24 100       95 @records = @{$records[0]} if(reftype($records[0]) eq 'ARRAY');
  21         60  
124 24 50       33 (grep { reftype($_) ne 'HASH' } @records) &&
  298         403  
125             die(__PACKAGE__."::mfsort: Can only sort hash-refs\n");
126              
127 24         34 my $sortsub = mfsortmaker($spec);
128 24         57 @records = sort { $sortsub->($a, $b) } @records;
  1132         2799  
129 24 100       249 return wantarray() ? @records : \@records;
130             }
131              
132             =head2 mfsortmaker
133              
134             This takes a sort spec subroutine reference like C but returns
135             a reference to a subroutine that you can use with the built-in C
136             function.
137              
138             my $sorter = mfsortmaker(sub {
139             author => 'asc',
140             title => 'asc'
141             });
142             @sorted = sort $sorter @unsorted;
143              
144             Note that you need to store the generated subroutine in a variable before
145             using it, otherwise the parser gets confused.
146              
147             Using this function to generate functions for C to use should be
148             considered to be experimental, as it can make some versions of perl
149             segfault. It appears to be reliable if you do this:
150              
151             my $sorter = mfsortmaker(...);
152             @sorted = sort { $sorter->($a, $b) } @unsorted;
153              
154             and that's what the C function does internally.
155              
156             =cut
157              
158             sub mfsortmaker {
159 26     26 1 411 my $spec = shift;
160 26         47 my @spec = $spec->();
161              
162 26     50   174 my $sortsub = sub($$) { 0 }; # default is to not sort at all
  50         104  
163 26         55 while(@spec) { # eat this from the end towards the beginning
164 37         58 my($spec, $field) = (pop(@spec), pop(@spec));
165 37 50       63 die(__PACKAGE__."::mfsortmaker: malformed spec after $field\n")
166             unless(defined($spec));
167 37 100       58 if(!ref($spec)) { # got a string
168 3341     3341   5273 $spec = ($spec =~ /^asc(ending)?$/i) ? sub { $_[0] cmp $_[1] } :
169 1092     1092   2274 ($spec =~ /^desc(ending)?$/i) ? sub { $_[1] cmp $_[0] } :
170 15     15   32 ($spec =~ /^numasc(ending)?$/i) ? sub { $_[0] <=> $_[1] } :
171 15     15   33 ($spec =~ /^numdesc(ending)?$/i) ? sub { $_[1] <=> $_[0] } :
172 33 50       217 die(__PACKAGE__."::mfsortmaker: Unknown shortcut '$spec'\n");
    100          
    100          
    100          
173             }
174 37         35 my $oldsortsub = $sortsub;
175             $sortsub = sub($$) {
176 5033 100   5033   5242 $spec->($_[0]->{$field}, $_[1]->{$field}) ||
177             $oldsortsub->($_[0], $_[1])
178             }
179 37         136 }
180             # extra layer of wrapping seems to prevent segfaults in 5.8.8. WTF?
181             # return $sortsub
182             return sub($$) {
183             # use Data::Dumper;print(map { Dumper($_) } @_);print "\n\n";
184 2018     2018   3848 $sortsub->(@_)
185 26         68 };
186             }
187              
188             =head1 BUGS, LIMITATIONS and FEEDBACK
189              
190             If you find undocumented bugs please report them either using
191             L or by email. Ideally, I would like to receive
192             sample data and a test file, which fails with the latest version of
193             the module but will pass when I fix the bug.
194              
195             For some unknown reason, passing C a particularly complex subroutine
196             generated using mfsortmaker can sometimes make perl 5.8.8 (and possibly
197             earlier versions) segfault. I *think* I've worked around it, and at least
198             it doesn't happen for me any more, but YMMV. It was something of a
199             Heisenbug so the current fix doesn't fill me with confidence.
200              
201             =cut
202              
203             =head1 SEE ALSO
204              
205             L for sorting data consisting of strings with fixed-length
206             fields in them.
207              
208             =head1 AUTHOR, COPYRIGHT and LICENCE
209              
210             Copyright 2008 David Cantrell Edavid@cantrell.org.ukE
211              
212             This software is free-as-in-speech software, and may be used,
213             distributed, and modified under the terms of either the GNU
214             General Public Licence version 2 or the Artistic Licence. It's
215             up to you which one you use. The full text of the licences can
216             be found in the files GPL2.txt and ARTISTIC.txt, respectively.
217              
218             =head1 CONSPIRACY
219              
220             This module is also free-as-in-mason software.
221              
222             =cut
223              
224             1;