7 # This is a perl6 implementation of L<S29/List/"=item sort">
13 * Clarify why this implementation of a spec'ed feature
14 exists in the "unspecced" directory of the test suite.
15 * That was where suggested when asked for suggestions
16 on #perl6. Other suggestions welcome.
22 * any Ordering has traits or only top level?
23 * {.TEST(:M)} is descending => &fuzzy_cmp is insensitive
26 * guidance on making this the builtin sort()
30 # L<S29/"List"/"=item sort">
32 my $prelude_sort = q:to 'END_PRELUDE_SORT';
33 subset KeyExtractor of Code where { .sig === :(Any --> Any) };
34 subset Comparator of Code where { .sig === :(Any, Any --> Int) };
36 of Pair where { .key ~~ KeyExtractor && .value ~~ Comparator };
38 of Signature | KeyExtractor | Comparator | OrderingPair;
40 module Prelude::Sort {
42 sub qby_cmp (Code @qby, $a, $b)
44 my $result = Order::Same;
45 my &return_ifn0 ::= -> $v { if $v { $result = $v; leave LOOP; } };
47 LOOP: for @by -> $cmpr {
48 return_ifn0 $cmpr($a, $b);
55 sub in_order (Code @qby, *$x, *@xs)
61 if by_cmp(@qby, $y, $z) > 0 {
73 sub qualify_by (Ordering @by)
75 my Any sub keyex (KeyExtractor $ex, Any $v) is cached
78 my Array sub sigkex (Signature $sig is copy, Any $v) is cached
79 { $sig := $v; @$sig; }
82 for @by -> $criterion {
84 my Signature $sig := $crierion;
85 my Array &kex := &sigkex;
90 for zip(@$a; @$b; @$sig) -> $x, $y, ::T {
94 if ( ::T ~~ canonicalized ) {
95 $u = ::T.canonicalized.($x);
96 $v = ::T.canonicalized.($y);
103 last if $value = $u cmp $v;
109 if ( $sig ~~ descending ) {
110 $cmpr = -> $a, $b { $cmpr($b, $a) };
114 $cmpr(kex($sig, $a), kex($sig, $b))
119 my KeyExtractor $ex := $criterion;
124 if ( $ex ~~ canonicalized ) {
125 $cmpr = -> $a is copy, $b is copy {
126 $a = $ex.canonicalized.($a);
127 $b = $ex.canonicalized.($b);
132 if ( $ex ~~ descending ) {
133 $cmpr = -> $a, $b { $cmpr($b, $a) };
136 take( -> $a, $b { $cmpr(kex($ex, $a), kex($ex, $b)) } );
140 my Comparator $cmpr = $criterion;
142 if ( $criterion ~~ insensitive ) {
144 $a = $criterion.canonicalized.($a);
145 $b = $criterion.canonicalized.($b);
150 if ( $criterion ~~ descending ) {
151 $cmpr = -> $a, $b { $cmpr($b, $a) };
158 my OrderingPair $scp := $criterion;
161 my KeyExtractor $ex = $scp.key;
162 my Comparator $cmpr = $scp.value;
164 if ( $pair ~~ canonicalized ) {
166 $a = $pair.canonicalized.($a);
167 $b = $pair.canonicalized.($b);
172 if ( $pair ~~ descending ) {
173 $cmpr = -> $a, $b { $cmpr($b, $a) };
176 take( -> $a, $b { $cmpr(kex($ex, $a), kex($ex, $b)) } );
188 sub mergesort (@values is rw, Ordering @by? = list(&infix:<cmp>),
193 my @qby = qualify_by(@by);
196 inplace_mergesort(@values, 0 => +@values, @qby);
201 inplace_mergesort(@copy, 0 => +@copy, @qby);
209 sub inplace_mergesort (@values is rw, Pair $span, Code @qby)
213 unless ( $span.value - $span.key == 1 || in_order(@qby, @values) ) {
214 my $mid = $span.key + int( ($span.value - $span.key)/ 2 );
218 inplace_mergesort(@values, $span.key => $mid, @qby),
219 inplace_mergesort(@values, $mid => $span.value, @qby),
228 sub merge (@values is rw, Pair $lspan, Pair $rspan, Code @qby)
230 # copy @left to a scratch area
231 my @scratch = @values[$lspan.key ..^ $lspan.value];
233 # merge @scratch and @right into and until @left is full
238 while ( $lc < $lspan.value ) {
239 @values[$lc++] = by_cmp(@qby, @scratch[$sc], @values[$rc]) <= 0
244 # at this point @left is full. start populating @right
245 # until @scratch or @right is empty
248 while ( $sc < +@scratch && $rc < $rspan.value ) {
249 @values[$ri++] = by_cmp(@qby, @scratch[$sc], @values[$rc]) <= 0
254 # anything remaining in @right is in the correct place.
255 # anything remaining in @scratch needs to be filled into @right
256 @values[$ri..^$rspan.value] = @scratch[$sc..^+@scratch];
258 # return the merged span
259 $lspan.key => $rspan.value;
263 our Array multi Array::p6sort( @values is rw, *&by, Bit $inplace? )
265 Prelude::Sort::mergesort(@values, list(&by), $inplace);
268 our Array multi Array::p6sort( @values is rw, Ordering @by, Bit $inplace? )
270 Prelude::Sort::mergesort(@values, @by, $inplace);
273 our Array multi Array::p6sort( @values is rw, Ordering $by = &infix:<cmp>,
276 Array::sort(@values, $by, $inplace);
279 our List multi List::p6sort( Ordering @by, *@values )
281 my @result = Prelude::Sort::mergesort(@values, @by);
285 our List multi List::p6sort( Ordering $by = &infix:<cmp>, *@values )
287 my @result = Prelude::Sort::mergesort(@values, list($by));
292 ok(eval($prelude_sort), 'prelude sort parses', :todo<sort>,
293 :depends<subset and argument list return signatures>);
297 ## sample() -- return a random sample of the input
298 sub sample (:$count, :$resample, *@data)
300 my $max = $count ?? $count !! +@data;
306 loop (my $i = 0; $i < $max; ++$i ) {
307 take @copy.splice((1..+@copy).pick, 1);
311 loop (my $i = 0; $i < $max; ++$i ) {
312 take @data[(1..+@data).pick];
318 my @num = sample 1..26;
319 my @str = sample 'a'..'z';
320 my @num_as_str = sample( '' >>~<< @num);
322 my @sorted_num = 1..26;
323 my @sorted_str = 'a'..'z';
324 my @sorted_num_as_str =
325 <1 10 11 12 13 14 15 16 17 18 19 2 20 21 22 23 24 25 26 3 4 5 6 7 8 9>;
331 my @sorted_things = map { Thingy.new( :name($_) ) },
332 ( reverse('N'..'Z'), reverse('a'..'m') );
334 my @unsorted_things = sample(@sorted_things);
339 ok(eval('@sorted = p6sort @str;'), 'parse of p6sort',
342 ok(@sorted eqv @sorted_str, 'string ascending; default cmp',
343 :todo, :depends<p6sort>);
349 ok(eval('@sorted = p6sort { $^a <=> $^b }, @num;'), 'parse of p6sort',
352 ok(@sorted eqv @sorted_num, 'number ascending; Comparator',
353 :todo, :depends<p6sort>);
359 ok(eval('@sorted = p6sort { lc $^b.name cmp lc $^a.name }, @unsorted_things;'),
360 'parse of p6sort', :todo<feature>);
362 ok(@sorted eqv reverse(@sorted_things), 'string descending; Comparator',
363 :todo, :depends<p6sort>);
369 ok(eval('@sorted = p6sort { $^b.name cmp $^a.name } is insensitive, @str;'),
370 'parse trait on block closure',
372 :depends<traits on block closures>);
375 ok(@sorted eqv reverse(@sorted_str),
376 'string descending; Comparator is insensitive',
377 :todo, :depends<p6sort>);
383 ok(eval('@sorted = p6sort { $^a.name cmp $^b.name } is descending is insensitive, @str;'),
384 'parse trait on block closure',
386 :depends<traits on block closures>);
388 ok(@sorted eqv reverse(@sorted_str),
389 'string descending; Comparator is descending is insensitive',
390 :todo, :depends<p6sort>);
393 # TODO: Modtimewise numerically ascending...
395 # my @files = sample { ... };
396 # my @sorted_files = qx( ls -t @files[] );
399 # my @sorted = p6sort { $^a.:M <=> $^b.:M }, @files;
401 # ok(@sorted eqv @sorted_files, 'number ascending; Comparator',
406 sub fuzzy_cmp($x, $y) returns Int
408 if ( 10 >= $x < 20 && 10 >= $y < 20 ) {
416 my @answer = 5..9, reverse(10..19), 20..24;
417 my @unsorted = sample(@answer);
421 ok(eval('@sorted = p6sort &fuzzy_cmp, @unsorted;'),
422 'parse of p6sort', :todo<feature>);
424 ok(@sorted eqv @answer, 'number fuzzy; Comparator', :todo,
431 ok(eval('@sorted = p6sort { + $^elem }, @num_as_str;'),
432 'parse of p6sort', :todo<feature>);
434 ok(@sorted eqv @sorted_num,
435 'number ascending; KeyExtractor uses context',
436 :todo, :depends<p6sort>);
438 ok(eval('@sorted = p6sort { + $_ }, @num_as_str;'),
439 'parse of p6sort', :todo<feature>);
441 ok(@sorted eqv @sorted_num,
442 'number ascending; KeyExtractor uses $_',
443 :todo, :depends<p6sort>);
450 my @sorted_things = map { Thingy.new( :name($_) ) },
451 ( reverse('N'..'Z'), reverse('a'..'m') );
453 my @unsorted_things = sample(@sorted_things);
458 ok(eval('@sorted = p6sort { ~ $^elem.name } is descending is insensitive, @unsorted_things;'),
459 'parse trait on block closure',
461 :depends<traits on block closures>);
463 ok(@sorted eqv @sorted_things,
464 'string descending; KeyExtractor is descending is insensitive',
465 :todo, :depends<p6sort>);
467 ok(eval('@sorted = p6sort { lc $^elem.name } is descending, @unsorted_things;'),
468 'parse trait on block closure',
470 :depends<traits on block closures>);
472 ok(@sorted eqv @sorted_things,
473 'string descending; KeyExtractor is descending uses context',
474 :todo, :depends<p6sort>);
476 ok(eval('@sorted = p6sort { lc .name } is descending, @unsorted_things;'),
477 'parse trait on block closure',
479 :depends<traits on block closures>);
481 ok(@sorted eqv @sorted_things,
482 'string descending; KeyExtractor is descending uses dot',
483 :todo, :depends<p6sort>);
487 # my @sorted = p6sort { .:M } @files;
489 # ok(@sorted eqv @sorted_files, 'number ascending; KeyExtractor',
493 sub get_key ($elem) { return $elem.name; }
498 ok(eval('@sorted = p6sort &get_key, @unsorted_things;'),
499 'parse of p6sort', :todo<feature>);
501 ok(@sorted eqv @sorted_things,
502 'string ascending; KeyExtractor via sub',
503 :todo, :depends<p6sort>);
506 my @numstr = sample( 1..3, 'A'..'C', 'x'..'z', 10..12 );
507 my @sorted_di_numstr = list(<z y x>, <C B A>, reverse(1..3, 10..12)),
512 # Not sure you can have traits on objects but
513 # L<S29/List/=item sort>
514 # says that any Ordering can have `descending` and `canonicalized($how)` traits.
515 ok(eval('@sorted = p6sort ( { $_ } => {
519 when Num { $^a <=> $^b }
520 default { $^a cmp $^b }
523 default { $^a cmp $^b }
525 }) is descending is canonicalized({$^v ~~ Str ?? lc($v) !! $v}),
527 'parse trait on object',
529 :depends<traits on objects>);
531 ok(@sorted eqv @sorted_di_numstr,
532 'Num|Str fuzzy; Pair is descending is insensitive',
533 :todo, :depends<p6sort>);
535 # @sorted = p6sort { $_ ~~ :M } => { $^b cmp $^a }, @files;
537 # ok(@sorted eqv @sorted_modtime_cmp_files,
538 # 'string descending; Pair uses cmp',
541 # @sorted = p6sort { $_ ~~ :M } => &fuzzy_cmp, @files;
543 # ok(@sorted eqv @sorted_modtime_fuzzy_files,
544 # 'number fuzzy; Pair',
547 # @sorted = p6sort ( { $_ ~~ :M } => { $^a cmp $^b } ) is descending, @files;
549 # ok(@sorted eqv @sorted_modtime_cmp_files,
550 # 'string descending; Pair is descending',
555 # Need to think about this one to create a meaningful dataset.
557 # # Numerically ascending
558 # # or else namewise stringifically descending case-insensitive
559 # # or else modtimewise numerically ascending
560 # # or else namewise fuzz-ifically
561 # # or else fuzz-ifically...
562 # @sorted = p6sort [ {+ $^elem},
563 # {$^b.name cmp $^a.name} is insensitive,
565 # {.name}=>&fuzzy_cmp,
570 # ok(@sorted eqv @sorted_whacky, 'obj whacky; @by', :todo<sort>);
576 ok(@inplace !eqv @sorted_str, 'sampled data differs from answer');
578 ok(eval('@inplace.p6sort(:inplace);', 'parse of p6sort with optional $inplace'),
581 ok(@inplace eqv @sorted_str, 'inplace sort on array', :todo,