File Coverage

blib/lib/Sort/BySpec.pm
Criterion Covered Total %
statement 68 77 88.3
branch 34 40 85.0
condition 12 15 80.0
subroutine 9 9 100.0
pod 2 2 100.0
total 125 143 87.4


line stmt bran cond sub pod time code
1             package Sort::BySpec;
2              
3             our $DATE = '2017-02-17'; # DATE
4             our $VERSION = '0.03'; # VERSION
5              
6 1     1   424 use 5.010001;
  1         2  
7 1     1   3 use strict 'subs', 'vars';
  1         1  
  1         19  
8 1     1   3 use warnings;
  1         1  
  1         19  
9              
10 1     1   2 use Exporter qw(import);
  1         1  
  1         704  
11             our @EXPORT_OK = qw(sort_by_spec cmp_by_spec);
12              
13             our %SPEC;
14              
15             $SPEC{':package'} = {
16             v => 1.1,
17             summary => 'Sort array (or create a list sorter) according to '.
18             'specification',
19             };
20              
21             $SPEC{sort_by_spec} = {
22             v => 1.1,
23             summary => 'Sort array (or create a list sorter) according to '.
24             'specification',
25             description => <<'_',
26              
27              
28             _
29             args => {
30             spec => {
31             schema => 'array*',
32             req => 1,
33             pos => 0,
34             },
35             xform => {
36             schema => 'code*',
37             summary => 'Code to return sort keys from data elements',
38             description => <<'_',
39              
40             This is just like `xform` in `Sort::ByExample`.
41              
42             _
43             },
44             reverse => {
45             summary => 'If set to true, will reverse the sort order',
46             schema => ['bool*', is=>1],
47             },
48             array => {
49             schema => 'array*',
50             },
51             },
52             result => {
53             summary => 'Sorted array, or sort coderef',
54             schema => ['any*', of=>['array*','code*']],
55             description => <<'_',
56              
57             If array is specified, will returned the sorted array. If array is not specified
58             in the argument, will return a sort subroutine that can be used to sort a list
59             and return the sorted list.
60              
61             _
62             },
63             result_naked => 1,
64             examples => [
65             {
66             summary => 'Sort according to a sequence of scalars (like Sort::ByExample)',
67             args => {
68             spec => ['foo', 'bar', 'baz'],
69             array => [1, 2, 3, 'bar', 'a', 'b', 'c', 'baz'],
70             },
71             },
72             {
73             summary => 'Like previous example, but reversed',
74             args => {
75             spec => ['foo', 'bar', 'baz'],
76             array => [1, 2, 3, 'bar', 'a', 'b', 'c', 'baz'],
77             reverse => 1,
78             },
79             },
80             {
81             summary => 'Put integers first (in descending order), then '.
82             'a sequence of scalars, then others (in ascending order)',
83             args => {
84             spec => [
85             qr/\A\d+\z/ => sub { $_[1] <=> $_[0] },
86             'foo', 'bar', 'baz',
87             qr// => sub { $_[0] cmp $_[1] },
88             ],
89             array => ["qux", "b", "a", "bar", "foo", 1, 10, 2],
90             },
91             },
92             ],
93             };
94             sub sort_by_spec {
95 8     8 1 10395 my %args = @_;
96              
97 8         14 my $spec = $args{spec};
98 8         11 my $xform = $args{xform};
99              
100             my $code_get_rank = sub {
101 69     69   52 my $val = shift;
102              
103 69         49 my $j;
104 69         94 for my $which (0..2) { # 0=scalar, 1=regexp, 2=code
105 152         98 $j = -1;
106 152         108 while ($j < $#{$spec}) {
  502         809  
107 389         285 $j++;
108 389         294 my $spec_elem = $spec->[$j];
109 389         737 my $ref = ref($spec_elem);
110 389 100       494 if (!$ref) {
    100          
    100          
111 311 100 100     739 if ($which == 0 && $val eq $spec_elem) {
112 22         51 return($j);
113             }
114             } elsif ($ref eq 'Regexp') {
115 38         32 my $sortsub;
116 38 100 66     23 if ($j < $#{$spec} && ref($spec->[$j+1]) eq 'CODE') {
  38         136  
117 29         41 $sortsub = $spec->[$j+1];
118             }
119 38 100 100     130 if ($which == 1 && $val =~ $spec_elem) {
120 9         35 return($j, $sortsub);
121             }
122 29 100       47 $j++ if $sortsub;
123             } elsif ($ref eq 'CODE') {
124 39         22 my $sortsub;
125 39 50 33     23 if ($j < $#{$spec} && ref($spec->[$j+1]) eq 'CODE') {
  39         107  
126 39         31 $sortsub = $spec->[$j+1];
127             }
128 39 100 100     55 if ($which == 2 && $spec_elem->($val)) {
129 7         33 return($j, $sortsub);
130             }
131 32 50       54 $j++ if $sortsub;
132             } else {
133 1         21 die "Invalid spec[$j]: not a scalar/Regexp/code";
134             }
135             } # loop element of spec
136             } # which
137 30         72 return($j+1);
138 8         41 };
139              
140 8 100       17 if ($args{_return_cmp}) {
141             my $cmp = sub {
142 14     14   47 my ($a, $b);
143              
144 14 50       19 if (@_ >= 2) {
145 14         15 $a = $_[0];
146 14         14 $b = $_[1];
147             } else {
148 0         0 my $caller = caller();
149 0         0 $a = ${"caller\::a"};
  0         0  
150 0         0 $b = ${"caller\::b"};
  0         0  
151             }
152              
153 14 50       27 if ($xform) {
154 0         0 $a = $xform->($a);
155 0         0 $b = $xform->($b);
156             }
157              
158 14 50       22 if ($args{reverse}) {
159 0         0 ($a, $b) = ($b, $a);
160             }
161              
162 14         23 my ($rank_a, $sortsub) = $code_get_rank->($a);
163 14         21 my ($rank_b ) = $code_get_rank->($b);
164              
165 14 100       28 if ($rank_a != $rank_b) {
166 8         20 return $rank_a <=> $rank_b;
167             }
168 6 50       49 return 0 unless $sortsub;
169 0         0 return $sortsub->($a, $b);
170 1         7 };
171 1         5 return $cmp;
172             } else {
173             # use schwartzian transform to speed sorting longer lists
174             my $sorter = sub {
175 40         114 return map { $_->[0] }
176             sort {
177 73 100       143 $a->[2] <=> $b->[2] ||
    100          
178             ($a->[3] ? $a->[3]($a->[1], $b->[1]) : 0) }
179             map {
180 7 100   7   15 my $x = $xform ? $xform->($_) : $_;
  41         61  
181 41         57 [$_, $x, $code_get_rank->($x)]
182             } @_;
183 7         18 };
184              
185 7 100       21 if ($args{array}) {
186 6         6 return [$sorter->(@{ $args{array} })];
  6         15  
187             }
188 1         4 return $sorter;
189             }
190             }
191              
192             $SPEC{cmp_by_spec} = do {
193             # poor man's "clone"
194             my $meta = { %{ $SPEC{sort_by_spec} } };
195             $meta->{summary} = 'Create a compare subroutine to be used in sort()';
196             $meta->{args} = { %{$meta->{args}} };
197             delete $meta->{args}{array};
198             $meta->{result} = {
199             schema => ['code*'],
200             };
201             delete $meta->{examples};
202             $meta;
203             };
204             sub cmp_by_spec {
205 1     1 1 733 sort_by_spec(
206             @_,
207             _return_cmp => 1,
208             );
209             }
210              
211             1;
212             # ABSTRACT: Sort array (or create a list sorter) according to specification
213              
214             __END__