File Coverage

blib/lib/Sort/Naturally.pm
Criterion Covered Total %
statement 131 147 89.1
branch 75 98 76.5
condition 17 39 43.5
subroutine 11 11 100.0
pod 2 2 100.0
total 236 297 79.4


line stmt bran cond sub pod time code
1              
2             require 5;
3             package Sort::Naturally; # Time-stamp: "2004-12-29 18:30:03 AST"
4             $VERSION = '1.03';
5             @EXPORT = ('nsort', 'ncmp');
6             require Exporter;
7             @ISA = ('Exporter');
8              
9 2     2   17753 use strict;
  2         4  
  2         82  
10 2     2   2210 use locale;
  2         1138  
  2         15  
11 2     2   2206 use integer;
  2         24  
  2         9  
12              
13             #-----------------------------------------------------------------------------
14             # constants:
15 2 50   2   158 BEGIN { *DEBUG = sub () {0} unless defined &DEBUG }
16              
17 2     2   12 use Config ();
  2         3  
  2         45  
18             BEGIN {
19             # Make a constant such that if a whole-number string is that long
20             # or shorter, we KNOW it's treatable as an integer
21 2     2   11 no integer;
  2         2  
  2         9  
22 2     2   288 my $x = length(256 ** $Config::Config{'intsize'} / 2) - 1;
23 2 50       20 die "Crazy intsize: <$Config::Config{'intsize'}>" if $x < 4;
24 2         97 eval 'sub MAX_INT_SIZE () {' . $x . '}';
25 2 50       10 die $@ if $@;
26 2         705 print "intsize $Config::Config{'intsize'} => MAX_INT_SIZE $x\n" if DEBUG;
27             }
28              
29             sub X_FIRST () {-1}
30             sub Y_FIRST () { 1}
31              
32             my @ORD = ('same', 'swap', 'asis');
33              
34             #-----------------------------------------------------------------------------
35             # For lack of a preprocessor:
36              
37             my($code, $guts);
38             $guts = <<'EOGUTS'; # This is the guts of both ncmp and nsort:
39              
40             if($x eq $y) {
41             # trap this expensive case first, and then fall thru to tiebreaker
42             $rv = 0;
43              
44             # Convoluted hack to get numerics to sort first, at string start:
45             } elsif($x =~ m/^\d/s) {
46             if($y =~ m/^\d/s) {
47             $rv = 0; # fall thru to normal comparison for the two numbers
48             } else {
49             $rv = X_FIRST;
50             DEBUG > 1 and print "Numeric-initial $x trumps letter-initial $y\n";
51             }
52             } elsif($y =~ m/^\d/s) {
53             $rv = Y_FIRST;
54             DEBUG > 1 and print "Numeric-initial $y trumps letter-initial $x\n";
55             } else {
56             $rv = 0;
57             }
58              
59             unless($rv) {
60             # Normal case:
61             $rv = 0;
62             DEBUG and print "<$x> and <$y> compared...\n";
63              
64             Consideration:
65             while(length $x and length $y) {
66              
67             DEBUG > 2 and print " <$x> and <$y>...\n";
68              
69             # First, non-numeric comparison:
70             $x2 = ($x =~ m/^(\D+)/s) ? length($1) : 0;
71             $y2 = ($y =~ m/^(\D+)/s) ? length($1) : 0;
72             # Now make x2 the min length of the two:
73             $x2 = $y2 if $x2 > $y2;
74             if($x2) {
75             DEBUG > 1 and printf " <%s> and <%s> lexically for length $x2...\n",
76             substr($x,0,$x2), substr($y,0,$x2);
77             do {
78             my $i = substr($x,0,$x2);
79             my $j = substr($y,0,$x2);
80             my $sv = $i cmp $j;
81             print "SCREAM! on <$i><$j> -- $sv != $rv \n" unless $rv == $sv;
82             last;
83             }
84              
85              
86             if $rv =
87             # The ''. things here force a copy that seems to work around a
88             # mysterious intermittent bug that 'use locale' provokes in
89             # many versions of Perl.
90             $cmp
91             ? $cmp->(substr($x,0,$x2) . '',
92             substr($y,0,$x2) . '',
93             )
94             :
95             scalar(( substr($x,0,$x2) . '' ) cmp
96             ( substr($y,0,$x2) . '' )
97             )
98             ;
99             # otherwise trim and keep going:
100             substr($x,0,$x2) = '';
101             substr($y,0,$x2) = '';
102             }
103              
104             # Now numeric:
105             # (actually just using $x2 and $y2 as scratch)
106              
107             if( $x =~ s/^(\d+)//s ) {
108             $x2 = $1;
109             if( $y =~ s/^(\d+)//s ) {
110             # We have two numbers here.
111             DEBUG > 1 and print " <$x2> and <$1> numerically\n";
112             if(length($x2) < MAX_INT_SIZE and length($1) < MAX_INT_SIZE) {
113             # small numbers: we can compare happily
114             last if $rv = $x2 <=> $1;
115             } else {
116             # ARBITRARILY large integers!
117              
118             # This saves on loss of precision that could happen
119             # with actual stringification.
120             # Also, I sense that very large numbers aren't too
121             # terribly common in sort data.
122              
123             # trim leading 0's:
124             ($y2 = $1) =~ s/^0+//s;
125             $x2 =~ s/^0+//s;
126             print " Treating $x2 and $y2 as bigint\n" if DEBUG;
127              
128             no locale; # we want the dumb cmp back.
129             last if $rv = (
130             # works only for non-negative whole numbers:
131             length($x2) <=> length($y2)
132             # the longer the numeral, the larger the value
133             or $x2 cmp $y2
134             # between equals, compare lexically!! amazing but true.
135             );
136             }
137             } else {
138             # X is numeric but Y isn't
139             $rv = Y_FIRST;
140             last;
141             }
142             } elsif( $y =~ s/^\d+//s ) { # we don't need to capture the substring
143             $rv = X_FIRST;
144             last;
145             }
146             # else one of them is 0-length.
147              
148             # end-while
149             }
150             }
151             EOGUTS
152              
153             sub maker {
154             my $code = $_[0];
155             $code =~ s/~COMPARATOR~/$guts/g || die "Can't find ~COMPARATOR~";
156 2 50 33 2 1 13 eval $code;
  2 50 100 2 1 4  
  2 50 33 269   12  
  2 50 0 10   13  
  2 50 33     12  
  2 100 66     10  
  269 100 33     8517  
  269 100 33     1093  
  0 100 100     0  
  269 100 33     677  
  0 100 0     0  
  0 100 33     0  
  0 50 66     0  
  0 50       0  
  269 100       338  
  269 100       259  
  269 100       243  
  269 0       450  
  0 50       0  
  269 100       487  
  269 100       430  
  269 100       440  
  269 100       330  
  269 50       898  
  20 50       26  
  47 50       101  
  10 100       15  
  37 100       41  
  37 100       40  
  29 100       37  
  29 100       31  
  173 100       214  
  269 100       468  
  203 50       194  
  203 50       185  
  203 100       767  
  227 100       192  
  227 100       721  
  227 0       655  
  227 50       439  
  227 100       376  
  217 100       173  
  217 100       678  
  30 100       40  
  30 50       39  
  30 50       33  
  30         58  
  30         41  
  187         265  
  187         244  
  197         675  
  114         202  
  114         257  
  81         72  
  81         323  
  81         340  
  0         0  
  0         0  
  0         0  
  0         0  
  33         34  
  33         40  
  43         50  
  43         54  
  269         233  
  269         706  
  269         223  
  269         732  
  10         10931  
  10         59  
  0         0  
  10         30  
  10         13  
  10         18  
  269         552  
  0         0  
  269         367  
  269         297  
  269         836  
  20         24  
  47         94  
  10         13  
  37         42  
  37         48  
  29         30  
  29         28  
  173         210  
  269         463  
  203         210  
  203         165  
  203         767  
  227         203  
  227         671  
  227         589  
  227         457  
  227         397  
  217         165  
  217         651  
  30         38  
  30         34  
  30         33  
  30         54  
  30         47  
  187         242  
  187         260  
  197         625  
  114         159  
  114         242  
  81         70  
  81         309  
  81         357  
  0         0  
  0         0  
  0         0  
  0         0  
  33         31  
  33         42  
  43         41  
  43         53  
  269         255  
  269         710  
  269         251  
  269         425  
  110         187  
  110         137  
  110         262  
157             die $@ if $@;
158             }
159              
160             ##############################################################################
161              
162             maker(<<'EONSORT');
163             sub nsort {
164             # get options:
165             my($cmp, $lc);
166             ($cmp,$lc) = @{shift @_} if @_ and ref($_[0]) eq 'ARRAY';
167              
168             return @_ unless @_ > 1 or wantarray; # be clever
169              
170             my($x, $x2, $y, $y2, $rv); # scratch vars
171              
172             # We use a Schwartzian xform to memoize the lc'ing and \W-removal
173              
174             map $_->[0],
175             sort {
176             if($a->[0] eq $b->[0]) { 0 } # trap this expensive case
177             else {
178              
179             $x = $a->[1];
180             $y = $b->[1];
181              
182             ~COMPARATOR~
183              
184             # Tiebreakers...
185             DEBUG > 1 and print " -<${$a}[0]> cmp <${$b}[0]> is $rv ($ORD[$rv])\n";
186             $rv ||= (length($x) <=> length($y)) # shorter is always first
187             || ($cmp and $cmp->($x,$y) || $cmp->($a->[0], $b->[0]))
188             || ($x cmp $y )
189             || ($a->[0] cmp $b->[0])
190             ;
191              
192             DEBUG > 1 and print " <${$a}[0]> cmp <${$b}[0]> is $rv ($ORD[$rv])\n";
193             $rv;
194             }}
195              
196             map {;
197             $x = $lc ? $lc->($_) : lc($_); # x as scratch
198             $x =~ s/\W+//s;
199             [$_, $x];
200             }
201             @_
202             }
203             EONSORT
204              
205             #-----------------------------------------------------------------------------
206             maker(<<'EONCMP');
207             sub ncmp {
208             # The guts are basically the same as above...
209              
210             # get options:
211             my($cmp, $lc);
212             ($cmp,$lc) = @{shift @_} if @_ and ref($_[0]) eq 'ARRAY';
213              
214             if(@_ == 0) {
215             @_ = ($a, $b); # bit of a hack!
216             DEBUG > 1 and print "Hacking in <$a><$b>\n";
217             } elsif(@_ != 2) {
218             require Carp;
219             Carp::croak("Not enough options to ncmp!");
220             }
221             my($a,$b) = @_;
222             my($x, $x2, $y, $y2, $rv); # scratch vars
223              
224             DEBUG > 1 and print "ncmp args <$a><$b>\n";
225             if($a eq $b) { # trap this expensive case
226             0;
227             } else {
228             $x = ($lc ? $lc->($a) : lc($a));
229             $x =~ s/\W+//s;
230             $y = ($lc ? $lc->($b) : lc($b));
231             $y =~ s/\W+//s;
232              
233             ~COMPARATOR~
234              
235              
236             # Tiebreakers...
237             DEBUG > 1 and print " -<$a> cmp <$b> is $rv ($ORD[$rv])\n";
238             $rv ||= (length($x) <=> length($y)) # shorter is always first
239             || ($cmp and $cmp->($x,$y) || $cmp->($a,$b))
240             || ($x cmp $y)
241             || ($a cmp $b)
242             ;
243              
244             DEBUG > 1 and print " <$a> cmp <$b> is $rv\n";
245             $rv;
246             }
247             }
248             EONCMP
249              
250             # clean up:
251             undef $guts;
252             undef &maker;
253              
254             #-----------------------------------------------------------------------------
255             1;
256              
257             ############### END OF MAIN SOURCE ###########################################
258             __END__