File Coverage

blib/lib/Sort/BySpec.pm
Criterion Covered Total %
statement 67 75 89.3
branch 33 38 86.8
condition 12 15 80.0
subroutine 9 9 100.0
pod 2 2 100.0
total 123 139 88.4


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