File Coverage

blib/lib/App/Sets/Sort.pm
Criterion Covered Total %
statement 99 104 95.1
branch 24 38 63.1
condition 9 12 75.0
subroutine 13 13 100.0
pod 0 2 0.0
total 145 169 85.8


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