File Coverage

blib/lib/Sort/ByExample.pm
Criterion Covered Total %
statement 52 52 100.0
branch 24 28 85.7
condition 11 15 73.3
subroutine 14 14 100.0
pod 3 3 100.0
total 104 112 92.8


line stmt bran cond sub pod time code
1 1     1   22619 use strict;
  1         3  
  1         38  
2 1     1   5 use warnings;
  1         2  
  1         82  
3             package Sort::ByExample;
4             # ABSTRACT: sort lists to look like the example you provide
5             $Sort::ByExample::VERSION = '0.007';
6             #pod =head1 SYNOPSIS
7             #pod
8             #pod use Sort::ByExample
9             #pod cmp => { -as => 'by_eng', example => [qw(first second third fourth)] },
10             #pod sorter => { -as => 'eng_sort', example => [qw(first second third fourth)] };
11             #pod
12             #pod my @output = eng_sort(qw(second third unknown fourth first));
13             #pod # --> first second third fourth unknown
14             #pod
15             #pod # ...or...
16             #pod
17             #pod my @output = sort by_eng qw(second third unknown fourth first);
18             #pod # --> first second third fourth unknown
19             #pod
20             #pod # ...or...
21             #pod
22             #pod my $sorter = Sort::ByExample::sbe(\@example);
23             #pod my @output = $sorter->( qw(second third unknown fourth first) );
24             #pod # --> first second third fourth unknown
25             #pod
26             #pod # ...or...
27             #pod
28             #pod my $example = [ qw(charlie alfa bravo) ];
29             #pod my @input = (
30             #pod { name => 'Bertrand', codename => 'bravo' },
31             #pod { name => 'Dracover', codename => 'zulu', },
32             #pod { name => 'Cheswick', codename => 'charlie' },
33             #pod { name => 'Elbereth', codename => 'yankee' },
34             #pod { name => 'Algernon', codename => 'alfa' },
35             #pod );
36             #pod
37             #pod my $fallback = sub {
38             #pod my ($x, $y) = @_;
39             #pod return $x cmp $y;
40             #pod };
41             #pod
42             #pod my $sorter = sbe(
43             #pod $example,
44             #pod {
45             #pod fallback => $fallback,
46             #pod xform => sub { $_[0]->{codename} },
47             #pod },
48             #pod );
49             #pod
50             #pod my @output = $sorter->(@input);
51             #pod
52             #pod # --> (
53             #pod # { name => 'Cheswick', codename => 'charlie' },
54             #pod # { name => 'Algernon', codename => 'alfa' },
55             #pod # { name => 'Bertrand', codename => 'bravo' },
56             #pod # { name => 'Elbereth', codename => 'yankee' },
57             #pod # { name => 'Dracover', codename => 'zulu', },
58             #pod # );
59             #pod
60             #pod =head1 DESCRIPTION
61             #pod
62             #pod Sometimes, you need to sort things in a pretty arbitrary order. You know that
63             #pod you might encounter any of a list of values, and you have an idea what order
64             #pod those values go in. That order is arbitrary, as far as actual automatic
65             #pod comparison goes, but that's the order you want.
66             #pod
67             #pod Sort::ByExample makes this easy: you give it a list of example input it should
68             #pod expect, pre-sorted, and it will sort things that way. If you want, you can
69             #pod provide a fallback sub for sorting unknown or equally-positioned data.
70             #pod
71             #pod =cut
72              
73 1     1   1268 use Params::Util qw(_HASHLIKE _ARRAYLIKE _CODELIKE);
  1         5555  
  1         92  
74 1         15 use Sub::Exporter -setup => {
75             exports => {
76             sbe => undef,
77             cmp => \'_build_cmp',
78             sorter => \'_build_sorter',
79             },
80 1     1   1077 };
  1         7740  
81              
82             #pod =method sorter
83             #pod
84             #pod my $sorter = Sort::ByExample->sorter($example, $fallback);
85             #pod my $sorter = Sort::ByExample->sorter($example, \%arg);
86             #pod
87             #pod The sorter method returns a subroutine that will sort lists to look more like
88             #pod the example list.
89             #pod
90             #pod C<$example> may be a reference to an array, in which case input will be sorted
91             #pod into the same order as the data in the array reference. Input not found in the
92             #pod example will be found at the end of the output, sorted by the fallback sub if
93             #pod given (see below).
94             #pod
95             #pod Alternately, the example may be a reference to a hash. Values are used to
96             #pod provide sort orders for input values. Input values with the same sort value
97             #pod are sorted by the fallback sub, if given.
98             #pod
99             #pod If given named arguments as C<%arg>, valid arguments are:
100             #pod
101             #pod fallback - a sub to sort data
102             #pod xform - a sub to transform each item into the key to sort
103             #pod
104             #pod If no other named arguments are needed, the fallback sub may be given in place
105             #pod of the arg hashref.
106             #pod
107             #pod The fallback sub should accept two inputs and return either 1, 0, or -1, like a
108             #pod normal sorting routine. The data to be sorted are passed as parameters. For
109             #pod uninteresting reasons, C<$a> and C<$b> can't be used.
110             #pod
111             #pod The xform sub should accept one argument and return the data by which to sort
112             #pod that argument. In other words, to sort a group of athletes by their medals:
113             #pod
114             #pod my $sorter = sbe(
115             #pod [ qw(Gold Silver Bronze) ],
116             #pod {
117             #pod xform => sub { $_[0]->medal_metal },
118             #pod },
119             #pod );
120             #pod
121             #pod If both xform and fallback are given, then four arguments are passed to
122             #pod fallback:
123             #pod
124             #pod a_xform, b_xform, a_original, b_original
125             #pod
126             #pod =method cmp
127             #pod
128             #pod my $comparitor = Sort::ByExample->cmp($example, \%arg);
129             #pod
130             #pod This routine expects the same sort of arguments as C>, but returns a
131             #pod subroutine that behaves like a C> comparitor. It will
132             #pod take two arguments and return 1, 0, or -1.
133             #pod
134             #pod C I be given an C argument or an exception will be
135             #pod raised. This behavior may change in the future, but because a
136             #pod single-comparison comparitor cannot efficiently perform a L
137             #pod transform|http://en.wikipedia.org/wiki/Schwartzian_transform>, using a
138             #pod purpose-build C> is a better idea.
139             #pod
140             #pod =head1 EXPORTS
141             #pod
142             #pod =head2 sbe
143             #pod
144             #pod C behaves just like C>, but is a function rather than a method.
145             #pod It may be imported by request.
146             #pod
147             #pod =head2 sorter
148             #pod
149             #pod The C export builds a function that behaves like the C method.
150             #pod
151             #pod =head2 cmp
152             #pod
153             #pod The C export builds a function that behaves like the C method.
154             #pod Because C requires a named sub, importing C can be very useful:
155             #pod
156             #pod use Sort::ByExample
157             #pod cmp => { -as => 'by_eng', example => [qw(first second third fourth)] };
158             #pod
159             #pod my @output = sort by_eng qw(second third unknown fourth first);
160             #pod # --> first second third fourth unknown
161             #pod
162             #pod =cut
163              
164 8     8 1 5340 sub sbe { __PACKAGE__->sorter(@_) }
165              
166             sub __normalize_args {
167 12     12   18 my ($self, $example, $arg) = @_;
168              
169 12         21 my $score = 0;
170 34         99 my %score = _HASHLIKE($example) ? %$example
171 12 100       434 : _ARRAYLIKE($example) ? (map { $_ => $score++ } @$example)
    100          
172             : Carp::confess "invalid example data given to Sort::ByExample";
173              
174 11         23 my $fallback;
175 11 100       31 if (_HASHLIKE($arg)) {
176 7         12 $fallback = $arg->{fallback};
177             } else {
178 4         7 $fallback = $arg;
179 4         9 $arg = {};
180             }
181              
182 11 50 66     71 Carp::croak "invalid fallback routine"
183             if $fallback and not _CODELIKE($fallback);
184              
185 11         37 return (\%score, $fallback, $arg);
186             }
187              
188             sub __cmp {
189 9     9   15 my ($self, $score, $fallback, $arg) = @_;
190              
191             return sub ($$) {
192 134     134   189 my ($a, $b) = @_;
193 134 100 100     866 (exists $score->{$a} && exists $score->{$b})
    100 66        
    100          
    100          
194             ? ($score->{$a} <=> $score->{$b}) || ($fallback ? $fallback->($a, $b) : 0)
195             : exists $score->{$a} ? -1
196             : exists $score->{$b} ? 1
197             : ($fallback ? $fallback->($a, $b) : 0)
198 9         53 };
199             }
200              
201             sub cmp {
202 2     2 1 3 my ($self, $example, $rest) = @_;
203              
204 2         6 my ($score, $fallback, $arg) = $self->__normalize_args($example, $rest);
205              
206 2 50       6 Carp::confess "you may not build a transformation into a comparitor"
207             if $arg->{xform};
208              
209 2         6 $self->__cmp($score, $fallback, $arg);
210             }
211              
212             sub sorter {
213 10     10 1 22 my ($self, $example, $rest) = @_;
214              
215 10         34 my ($score, $fallback, $arg) = $self->__normalize_args($example, $rest);
216              
217 9 100       31 if (my $xf = $arg->{xform}) {
218             return sub {
219 10 50 100     21 map { $_->[1] }
  14 50 33     112  
    100          
    100          
220             sort {
221 10         46 (exists $score->{$a->[0]} && exists $score->{$b->[0]})
222             ? ($score->{$a->[0]} <=> $score->{$b->[0]})
223             || ($fallback ? $fallback->($a->[0], $b->[0], $a->[1], $b->[1]) : 0)
224             : exists $score->{$a->[0]} ? -1
225             : exists $score->{$b->[0]} ? 1
226             : ($fallback ? $fallback->($a->[0], $b->[0], $a->[1], $b->[1]) : 0)
227 2     2   12 } map { [ $xf->($_), $_ ] } @_;
228             }
229 2         14 }
230              
231 7         24 my $cmp = $self->__cmp($score, $fallback, $arg);
232              
233 6     6   3327 sub { sort { $cmp->($a, $b) } @_ }
  106         229  
234 7         40 }
235              
236             sub _build_sorter {
237 2     2   272 my ($self, $name, $arg) = @_;
238 2         6 my ($example) = $arg->{example};
239 2         4 local $arg->{example};
240              
241 2         7 $self->sorter($example, $arg);
242             }
243              
244             sub _build_cmp {
245 2     2   1560 my ($self, $name, $arg) = @_;
246 2         5 my ($example) = $arg->{example};
247 2         6 local $arg->{example};
248              
249 2         6 $self->cmp($example, $arg);
250             }
251              
252             #pod =head1 TODO
253             #pod
254             #pod =for :list
255             #pod * provide a way to say "these things occur after any unknowns"
256             #pod
257             #pod =cut
258              
259             1;
260              
261             __END__