File Coverage

blib/lib/Text/CSV/Separator.pm
Criterion Covered Total %
statement 126 150 84.0
branch 60 82 73.1
condition 17 24 70.8
subroutine 10 11 90.9
pod 1 1 100.0
total 214 268 79.8


line stmt bran cond sub pod time code
1             package Text::CSV::Separator;
2            
3 3     3   84429 use 5.008;
  3         12  
  3         126  
4 3     3   18 use strict;
  3         6  
  3         113  
5 3     3   18 use warnings;
  3         10  
  3         131  
6 3     3   16 use Carp qw(carp croak);
  3         7  
  3         320  
7            
8             our $VERSION = '0.20';
9            
10 3     3   15 use Exporter;
  3         12  
  3         117  
11 3     3   17 use base 'Exporter';
  3         4  
  3         6238  
12             our @EXPORT_OK = qw(get_separator);
13            
14            
15             sub get_separator {
16            
17 18     18 1 27997 my %options = @_;
18            
19 18         47 my $file_path = $options{path};
20            
21             # check options
22 18         25 my $echo;
23 18 50       71 if ($options{echo}) {
24 0         0 $echo = 1;
25 0         0 print "\nDetecting field separator of $file_path\n";
26             }
27            
28 18         31 my (@excluded, @included);
29 18 100       63 if (exists $options{exclude}) {
30 6         11 @excluded = @{$options{exclude}};
  6         23  
31             }
32            
33 18 100       56 if (exists $options{include}) {
34 2         4 @included = @{$options{include}};
  2         9  
35             }
36            
37 18         27 my ($lucky, $colon_timecol, $comma_decsep, $comma_groupsep);
38 18 100 66     97 if (exists $options{lucky} && $options{lucky} == 1) {
39 9         11 $lucky = 1;
40 9 50       20 print "Scalar context...\n\n" if $echo;
41             } else {
42 9         20 $colon_timecol = $comma_decsep = $comma_groupsep = 1;
43 9 50       24 print "List context...\n\n" if $echo;
44             }
45            
46             # options checked
47            
48             # Default set of candidates
49 18         51 my @candidates = (',', ';', ':', '|', "\t");
50            
51 18         22 my %survivors;
52 18         521 $survivors{$_} = [] foreach (@candidates);
53            
54 18 100       60 if (@excluded > 0) {
55 6         168 foreach (@excluded) {
56 12         23 delete $survivors{$_};
57 12 50       42 _message('deleted', $_) if $echo;
58             }
59             }
60            
61 18 100       49 if (@included > 0) {
62 2         7 foreach (@included) {
63 2 50       11 if (length($_) == 1) {
64 2         7 $survivors{$_} = [];
65             }
66 2 50       9 _message('added', $_) if $echo;
67             }
68             }
69            
70 18 50       58 if (keys %survivors == 0) {
71 0         0 carp "No candidates left!";
72 0         0 return;
73             }
74            
75 18         21 my $csv;
76 18 50       3089 open ($csv, "<:crlf", $file_path) || croak "Couldn't open $file_path: $!";
77            
78 18         132 my $record_count = 0; # if $echo
79 18         376 while (<$csv>) {
80 5134         7600 my $record = $_;
81 5134         6479 chomp $record;
82            
83 5134 50       10300 if ($echo) {
84 0         0 $record_count++;
85 0         0 print "\nRecord #$record_count\n";
86             }
87            
88 5134         9815 foreach my $candidate (keys %survivors) {
89 10314 50       18588 _message('candidate', $candidate) if $echo;
90            
91 10314         102109 my $rex = qr/\Q$candidate\E/;
92            
93 10314         16209 my $count = 0;
94 10314         135991 $count++ while ($record =~ /$rex/g);
95            
96 10314 50       21425 print "Count: $count\n" if $echo;
97            
98 10314 100 100     49434 if ($count > 0 && !$lucky) {
    100          
99 5130         5423 push @{$survivors{$candidate}}, $count;
  5130         17150  
100             } elsif ($count == 0) {
101 54         154 delete $survivors{$candidate};
102             }
103            
104             }
105            
106 5134 100       11698 if (!$lucky) {
107 2567 100       5635 $colon_timecol = _regularity($record, 'timecol') if $colon_timecol;
108 2567 100       6150 $comma_decsep = _regularity($record, 'decsep') if $comma_decsep;
109 2567 100       5054 $comma_groupsep = _regularity($record, 'groupsep') if $comma_groupsep;
110             }
111            
112            
113 5134         11906 my @alive = keys %survivors;
114 5134         8237 my $survivors_count = @alive;
115 5134 100       25917 if ($survivors_count == 1) {
    100          
116 8 50       25 if ($echo) {
117 0         0 _message('detected', $alive[0]);
118 0         0 print "Returning control to caller...\n\n";
119             }
120 8         276 close $csv;
121 8 100       18 if (!$lucky) {
122 4         985 return @alive;
123             } else {
124 4         34 return $alive[0];
125             }
126             } elsif ($survivors_count == 0) {
127 2         579 carp "\nNo candidates left!\n";
128 2         479 return;
129             }
130             }
131            
132             # More than 1 survivor. 2nd pass to determine count variability
133 8 100       24 if ($lucky) {
134 4 50       9 print "\nSeveral candidates left\n" if $echo;
135 4         934 carp "\nBad luck. Couldn't determine the separator of $file_path.\n";
136 4         529 return;
137             } else {
138 4 50       11 print "\nVariability:\n\n" if $echo;
139 4         6 my %std_dev;
140 4         11 foreach my $candidate (keys %survivors) {
141 9         14 my $mean = _mean(@{$survivors{$candidate}});
  9         206  
142 9         14 $std_dev{$candidate} = _std_dev($mean, @{$survivors{$candidate}});
  9         124  
143 9 50       32 if ($echo) {
144 0         0 _message('candidate', $candidate);
145 0         0 print "Mean: $mean\tStd Dev: $std_dev{$candidate}\n\n";
146             }
147             }
148            
149 4 50       10 print "Couldn't determine the separator\n" if $echo;
150            
151 4         119 close $csv;
152            
153 4         6 my @penalized;
154 4 100       11 if ($colon_timecol) {
155 1 50       4 print "Detected time column\n" if $echo;
156 1         23 delete $survivors{':'};
157 1         3 push @penalized, ':';
158             }
159            
160 4 100 66     21 if ($comma_decsep || $comma_groupsep) {
161 1         20 delete $survivors{','};
162 1         4 push @penalized, ',';
163 1 50 33     6 if ($echo && $comma_decsep) {
164 0         0 print "\nDetected comma-separated decimal numbers column\n";
165             }
166 1 50 33     6 if ($echo && $comma_groupsep) {
167 0         0 print "\nDetected comma-grouped numbers column\n";
168             }
169             }
170            
171 4         23 my @alive = sort {$std_dev{$a} <=> $std_dev{$b}} keys %survivors;
  4         15  
172 4         11 push @alive, sort {$std_dev{$a} <=> $std_dev{$b}} @penalized;
  0         0  
173 4 50       10 if ($echo) {
174 0         0 print "Remaining candidates: ";
175 0         0 foreach my $left (@alive) {
176 0         0 _message('left', $left);
177             }
178 0         0 print "\n\nReturning control to caller...\n\n";
179             }
180 4         124 return @alive;
181             }
182             }
183            
184             sub _mean {
185 9     9   427 my @array = @_;
186            
187 9         11 my $sum = 0;
188 9         773 $sum += $_ foreach (@array);
189            
190 9         25 my $mean = $sum / scalar(@array);
191            
192 9         117 return $mean;
193             }
194            
195             sub _std_dev {
196 9     9   306 my ($mean, @array) = @_;
197            
198 9         317 my $sum = 0;
199 9         1602 $sum += ($_ - $mean)**2 foreach (@array);
200            
201 9         35 my $std_dev = sqrt( $sum / scalar(@array) );
202            
203 9         121 return $std_dev;
204             }
205            
206             sub _regularity {
207 2579     2579   5415 my ($string, $kind) = @_;
208            
209 2579         6393 my $time_rx = qr/
210             (?:^|(?<=\s|[T,;|\t]))
211             (?:[01]?[0-9]|2[0-3]) # hours
212             :
213             (?:[0-5][0-9]) # minutes
214             (?::[0-5][0-9])? # seconds
215             (?:
216             Z
217             |
218             \.\d+
219             |
220             (?:\+|-)
221             (?:[01]?[0-9]|2[0-3])
222             :
223             (?:[0-5][0-9])
224             )?
225             (?=$|\s|[,;|\t])
226             /x;
227            
228 2579         5704 my $commadecsep_rx = qr/
229             (?:^|(?<=[^\d,.]))
230             (?:
231             [-+]?
232             (?:
233             \d{0,3}?(?:\.\d{3})*
234             |
235             \d+
236             )
237             ,\d+
238             )
239             (?=$|[^\d,.])
240             /x;
241            
242 2579         5599 my $commagroupsep_rx = qr/
243             (?:^|(?<=[^\d,.]))
244             (?:
245             [-+]?\d{0,3}?
246             (?:,\d{3})+
247             (?:\.\d+)?
248             )
249             (?=$|[^\d,.])
250             /x;
251            
252            
253 2579 100 100     15823 return 0 if ($kind eq 'timecol' && $string !~ /$time_rx/);
254 2571 100 100     17787 return 0 if ($kind eq 'decsep' && $string !~ /$commadecsep_rx/);
255 2563 100 66     7666 return 0 if ($kind eq 'groupsep' && $string !~ /$commagroupsep_rx/);
256            
257 2554         7608 return 1;
258             }
259            
260             sub _message {
261 0     0     my ($type, $candidate) = @_;
262            
263 0           my $char;
264 0 0         if (ord $candidate == 9) { # tab character
265 0           $char = "\\t";
266             } else {
267 0           $char = $candidate;
268             }
269            
270 0           my %message = (
271             deleted => "Deleted $char from candidates list\n",
272             added => "Added $char to candidates list\n",
273             candidate => "Candidate: $char\t",
274             detected => "\nSeparator detected: $char\n",
275             left => " $char ",
276             );
277            
278 0           print $message{$type};
279             }
280            
281             1;
282            
283             __END__