File Coverage

blib/lib/App/Sets/Sort.pm
Criterion Covered Total %
statement 98 103 95.1
branch 25 40 62.5
condition 9 12 75.0
subroutine 13 13 100.0
pod 0 2 0.0
total 145 170 85.2


line stmt bran cond sub pod time code
1             package App::Sets::Sort;
2             $App::Sets::Sort::VERSION = '0.974';
3              
4              
5 4     4   28651 use strict;
  4         4  
  4         86  
6 4     4   12 use warnings;
  4         4  
  4         104  
7              
8             # ABSTRACT: sort handling
9              
10 4     4   11 use English qw( -no_match_vars );
  4         4  
  4         13  
11 4     4   1084 use 5.010;
  4         9  
12 4     4   2324 use File::Temp qw< tempfile >;
  4         43377  
  4         199  
13 4     4   19 use Fcntl qw< :seek >;
  4         4  
  4         340  
14 4     4   478 use Log::Log4perl::Tiny qw< :easy :dead_if_first >;
  4         5000  
  4         22  
15 4     4   732 use base 'Exporter';
  4         6  
  4         2720  
16              
17             our @EXPORT_OK = qw< sort_filehandle internal_sort_filehandle >;
18             our @EXPORT = qw< sort_filehandle >;
19             our %EXPORT_TAGS = (
20             default => [ @EXPORT ],
21             all => [ @EXPORT_OK ],
22             );
23              
24             sub _test_external_sort {
25 2     2   2 my $filename;
26              
27 2 50       2 eval {
28 2         7 (my $fh, $filename) = tempfile(); # might croak
29 2         607 binmode $fh, ':raw';
30 2 50       4 print {$fh} "one\ntwo\nthree\nfour\n" or die 'whatever';
  2         24  
31 2 50       64 close $fh or die 'whatever';
32             } or return;
33              
34 2 50       2749 open my $fh, '-|', 'sort', '-u', $filename or return;
35 2 50       13 return unless $fh;
36 2         2052 my @lines = <$fh>;
37 2 50       14 return unless scalar(@lines) == 4;
38 2 50       9 return unless defined $lines[3];
39 2         18 $lines[3] =~ s{\s+}{}gmxs;
40 2 50       8 return unless $lines[3] eq 'two';
41              
42 2         64 return 1;
43             }
44              
45             sub sort_filehandle {
46 129     129 0 741 my ($filename, $config) = @_;
47 129   100     190 $config ||= {};
48 129   66     108 state $has_sort = (!$config->{internal_sort}) && _test_external_sort();
49              
50 129 100       180 if ($has_sort) {
51 65         47 my $fh;
52 65 50       55 eval { open $fh, '-|', 'sort', '-u', $filename } and return $fh;
  65         71554  
53 0         0 WARN 'cannot use system sort, falling back to internal implementation';
54 0         0 $has_sort = 0; # from now on, use internal sort
55             }
56              
57 64         80 return internal_sort_filehandle($filename);
58             }
59              
60             sub internal_sort_filehandle {
61 65     65 0 117 my ($filename) = @_;
62              
63             # Open input stream
64 65 50       1289 open my $ifh, '<', $filename
65             or LOGDIE "open('$filename'): $OS_ERROR";
66              
67             # Maximum values hints taken from Perl Power Tools' sort
68 65   100     191 my $max_records = $ENV{SETS_MAX_RECORDS} || 200_000;
69 65   100     132 my $max_files = $ENV{SETS_MAX_FILES} || 40;
70 65         51 my (@records, @fhs);
71 65         514 while (<$ifh>) {
72 618         380 chomp;
73 618         467 push @records, $_;
74 618 100       1225 if (@records >= $max_records) {
75 3         5 push @fhs, _flush_to_temp(\@records);
76 3 50       12 _compact(\@fhs) if @fhs >= $max_files - 1;
77             }
78             }
79              
80 65 50       134 push @fhs, _flush_to_temp(\@records) if @records;
81 65         104 _compact(\@fhs);
82 65 50       643 return $fhs[0] if @fhs;
83              
84             # seems like the file was empty... so it's sorted
85 0         0 seek $ifh, 0, SEEK_SET;
86 0         0 return $ifh;
87             }
88              
89             sub _flush_to_temp {
90 68     68   59 my ($records) = @_;
91 68         129 my $tfh = tempfile(UNLINK => 1);
92 68         23483 my $previous;
93 68         279 for my $item (sort @$records) {
94 618 50 33     792 next if defined($previous) && $previous eq $item;
95 618         354 print {$tfh} $item, $INPUT_RECORD_SEPARATOR;
  618         1135  
96             }
97 68         116 @$records = ();
98 68         1241 seek $tfh, 0, SEEK_SET;
99 68         124 return $tfh;
100             }
101              
102             sub _compact {
103 68     68   62 my ($fhs) = @_;
104 68 100       121 return if @$fhs == 1;
105              
106             # where the output will end up
107 3         8 my $ofh = tempfile(UNLINK => 1);
108              
109             # convenience hash for tracking all contributors
110             my %its = map {
111 3         972 my $fh = $fhs->[$_];
  6         7  
112 6         21 my $head = <$fh>;
113 6 50       8 if (defined $head) {
114 6         6 chomp($head);
115 6         30 $_ => [ $fh, $head ];
116             }
117 0         0 else { () }
118             } 0 .. $#$fhs;
119              
120             # iterate until all contributors are exhausted
121 3         9 while (scalar keys %its) {
122              
123             # select the best (i.e. "lower"), cleanup on the way
124 25         28 my ($fk, @keys) = keys %its;
125 25         21 my $best = $its{$fk}[1];
126 25         23 for my $key (@keys) {
127 14         8 my $head = $its{$key}[1];
128 14 100       26 $best = $head if $best gt $head;
129             }
130 25         16 print {$ofh} $best, $INPUT_RECORD_SEPARATOR;
  25         50  
131              
132             # get rid of the best in all iterators, cleanup on the way
133             KEY:
134 25         20 for my $key ($fk, @keys) {
135 39         29 my $head = $its{$key}[1];
136 39         54 while ($head eq $best) {
137 25         33 $head = readline $its{$key}[0];
138 25 100       25 if (defined $head) {
139 19         42 chomp($its{$key}[1] = $head);
140             }
141             else {
142 6         9 delete $its{$key};
143 6         11 next KEY;
144             }
145             }
146             }
147             }
148              
149             # rewind, finalize compacting, return
150 3         54 seek $ofh, 0, SEEK_SET;
151 3         5 @$fhs = ($ofh);
152 3         9 return;
153             }
154              
155             1;
156              
157             __END__