File Coverage

blib/lib/Sort/BySpec.pm
Criterion Covered Total %
statement 68 77 88.3
branch 33 40 82.5
condition 12 15 80.0
subroutine 9 9 100.0
pod 2 2 100.0
total 124 143 86.7


line stmt bran cond sub pod time code
1             package Sort::BySpec;
2              
3             our $DATE = '2021-05-01'; # DATE
4             our $VERSION = '0.040'; # VERSION
5              
6 1     1   692 use 5.010001;
  1         9  
7 1     1   6 use strict 'subs', 'vars';
  1         2  
  1         43  
8 1     1   5 use warnings;
  1         2  
  1         39  
9              
10 1     1   6 use Exporter qw(import);
  1         12  
  1         1200  
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 19819 my %args = @_;
96              
97 8         17 my $spec = $args{spec};
98 8         14 my $xform = $args{xform};
99              
100             my $code_get_rank = sub {
101 69     69   98 my $val = shift;
102              
103 69         77 my $j;
104 69         105 for my $which (0..2) { # 0=scalar, 1=regexp, 2=code
105 152         183 $j = -1;
106 152         183 while ($j < $#{$spec}) {
  502         842  
107 389         480 $j++;
108 389         485 my $spec_elem = $spec->[$j];
109 389         505 my $ref = ref($spec_elem);
110 389 100       636 if (!$ref) {
    100          
    100          
111 311 100 100     704 if ($which == 0 && $val eq $spec_elem) {
112 22         68 return($j);
113             }
114             } elsif ($ref eq 'Regexp') {
115 38         43 my $sortsub;
116 38 100 66     46 if ($j < $#{$spec} && ref($spec->[$j+1]) eq 'CODE') {
  38         118  
117 29         42 $sortsub = $spec->[$j+1];
118             }
119 38 100 100     174 if ($which == 1 && $val =~ $spec_elem) {
120 9         43 return($j, $sortsub);
121             }
122 29 100       58 $j++ if $sortsub;
123             } elsif ($ref eq 'CODE') {
124 39         46 my $sortsub;
125 39 50 33     45 if ($j < $#{$spec} && ref($spec->[$j+1]) eq 'CODE') {
  39         142  
126 39         55 $sortsub = $spec->[$j+1];
127             }
128 39 100 100     81 if ($which == 2 && $spec_elem->($val)) {
129 7         40 return($j, $sortsub);
130             }
131 32 50       67 $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         71 return($j+1);
138 8         59 };
139              
140 8 100       27 if ($args{_return_cmp}) {
141             my $cmp = sub {
142 14     14   56 my ($a, $b);
143              
144 14 50       22 if (@_ >= 2) {
145 14         21 $a = $_[0];
146 14         16 $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       25 if ($xform) {
154 0         0 $a = $xform->($a);
155 0         0 $b = $xform->($b);
156             }
157              
158 14 50       25 if ($args{reverse}) {
159 0         0 ($a, $b) = ($b, $a);
160             }
161              
162 14         21 my ($rank_a, $sortsub) = $code_get_rank->($a);
163 14         19 my ($rank_b ) = $code_get_rank->($b);
164              
165 14 100       24 if ($rank_a != $rank_b) {
166 8         16 return $rank_a <=> $rank_b;
167             }
168 6 50       23 return 0 unless $sortsub;
169 0         0 return $sortsub->($a, $b);
170 1         5 };
171 1         4 return $cmp;
172             } else {
173             # use schwartzian transform to speed sorting longer lists
174             my $sorter = sub {
175 40         158 return map { $_->[0] }
176             sort {
177 73 100       200 $a->[2] <=> $b->[2] ||
    50          
178             ($a->[3] ? $a->[3]($a->[1], $b->[1]) : 0) }
179             map {
180 7 100   7   19 my $x = $xform ? $xform->($_) : $_;
  41         71  
181 41         67 [$_, $x, $code_get_rank->($x)]
182             } @_;
183 7         20 };
184              
185 7 100       23 if ($args{array}) {
186 6         10 return [$sorter->(@{ $args{array} })];
  6         16  
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 1374 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__