File Coverage

blib/lib/Text/NumericData.pm
Criterion Covered Total %
statement 208 232 89.6
branch 106 152 69.7
condition 17 24 70.8
subroutine 14 16 87.5
pod 7 15 46.6
total 352 439 80.1


line stmt bran cond sub pod time code
1             package Text::NumericData;
2              
3 14     14   52208 use Storable qw(dclone);
  14         48605  
  14         50714  
4              
5             # TODO: optimize those regexes, compile once in constructor
6              
7             # major.minor.bugfix, the latter two with 3 digits each
8             # It's not pretty, but I gave up on 1.2.3 style.
9             our $VERSION = '2.003003';
10             our $version = $VERSION;
11             $VERSION = eval $VERSION;
12              
13             our $years = '2005-2017';
14             our $copyright = 'Copyright (c) '.$years.' Thomas Orgis, Free Software licensed under the same terms as Perl 5.10';
15             our $author = 'Thomas Orgis ';
16              
17             # TODO: More smarts in separator search.
18             # One should find ', ' as separator in
19             # a / c, d /e
20             my $newhite = '[^\S\015\012]'; # whitespace that is no line end character
21             my $trenner = $newhite.'+|,'.$newhite.'*|;'.$newhite.'*';
22             my $ntrenner = '[^\s,;]+'; # also excludes CR LF
23             my $lend = '[\012\015]+';
24             my $nlend = '[^\012\015]';
25             my $quotematch = "['\"]";
26              
27             my %endname = ("\015\012"=>'DOS', "\012"=>'UNIX', "\015"=>'MAC');
28             my %endstring = reverse %endname;
29              
30             # Fallback defaults if anything else fails.
31             our $default_sep = "\t";
32             our $default_eol = $/;
33             our $default_comchar = '#';
34             our $default_quote = 1;
35             our $default_quotechar = '"';
36              
37             our %help =
38             (
39             separator=>'use this separator for input (otherwise deduce from data; TAB is another way to say "tabulator", fallback is'.$default_sep.')'
40             ,outsep=>'use this separator for output (leave undefined to use input separator, fallback to '.($default_sep eq "\t" ? 'TAB' : $default_sep).')'
41             ,lineend=>'line ending to use: ('.join(', ', sort keys %endstring).' or be explicit if you can, taken from data if undefined, finally resorting to '.(defined $endname{$default_eol} ? $endname{$default_eol} : $default_eol).')'
42             ,comchar=>'comment character (if not set, deduce from data or use '.$default_comchar.')'
43             ,numregex=>'regex for matching numbers'
44             ,numformat=>'printf formats to use (if there is no "%" present at all, one will be prepended)'
45             ,comregex=>'regex for matching comments'
46             ,quote=>'quote titles'
47             ,quotechar=>'quote character to use (derived from input or '.$default_quotechar.')'
48             ,strict=>'strictly split data lines at configured separator (otherwise more fuzzy logic is involved)'
49             ,text=>'allow text as data (not first column)'
50             ,fill=>'fill value for undefined data'
51             ,black=>'ignore whitespace at beginning and end of line (disables strict mode)'
52             ,empty=>'treat empty lines as empty data sets, preserving them in output'
53             );
54              
55             # These are defaults for user settings.
56             our %defaults =
57             (
58             'separator',undef,
59             'outsep', undef,
60             'lineend', undef,
61             'comchar', undef,
62             'numregex', '[\+\-]?\d*\.?\d*[eE]?\+?\-?\d*',
63             'numformat',[],
64             'comregex','[#%]*'.$newhite.'*',
65             'quote',undef,
66             'quotechar',undef,
67             'strict', 0,
68             'text', 1
69             ,'fill',undef # a value to fill in for non-existent but still demanded data
70             ,'black', 0
71             ,'empty',0
72             );
73              
74             sub new
75             {
76 57     57 0 148 my $class = shift;
77 57         182 my $self = {};
78 57         156 bless $self, $class;
79             # Only pick the parts of the config hash that are of interest here.
80 57         123 my $gotconf = shift;
81 57         252 $self->{gotconfig} = {};
82 57         346 for my $n (keys %defaults)
83             {
84 798 100       1812 if(ref $gotconf->{$n})
85             {
86 55         1727 $self->{gotconfig}{$n} = dclone($gotconf->{$n});
87             }
88             else
89             {
90 743         1863 $self->{gotconfig}{$n} = $gotconf->{$n};
91             }
92             }
93 57         189 foreach my $n (@{$self->{gotconfig}->{numformat}})
  57         224  
94             {
95 41 100       243 $n = '%'.$n unless $n =~ /\%/;
96             }
97             # Expand named special characters for line ending.
98 57 100       249 if(defined $gotconf->{lineend})
99             {
100             $self->{gotconfig}{lineend} = defined $endstring{$gotconf->{lineend}}
101             ? $endstring{$gotconf->{lineend}}
102 34 100       218 : $gotconf->{lineend};
103             }
104 57         168 for (qw(separator outsep))
105             {
106 114 100 100     530 if(defined $gotconf->{$_} and $gotconf->{$_} eq 'TAB'){ $self->{gotconfig}{$_} = "\t"; }
  2         4  
107             }
108              
109             $self->{gotconfig}{strict} = 0
110 57 100       205 if $self->{gotconfig}{black};
111 57         314 $self->init();
112 57         372 return $self;
113             }
114              
115             sub init
116             {
117 74     74 0 160 my $self = shift;
118 74         146 %{$self->{config}} = %{$self->{gotconfig}};
  74         564  
  74         398  
119 74         294 $self->{comments} = []; #some comment in header
120 74         191 $self->{guessquote} = undef;
121 74         186 $self->{titles} = []; #column titles
122 74         216 $self->{title} = undef; #file title
123 74         198 foreach my $k ('numregex','numformat','comregex','fill')
124             {
125 296 100       970 $self->{config}{$k} = $defaults{$k} unless defined $self->{config}{$k};
126             }
127             # Strict mode needs some set separator.
128 74 100 100     349 if($self->{config}{strict} and not defined $self->{config}{separator})
129             {
130 1         4 $self->{config}{separator} = $default_sep;
131             }
132             }
133              
134             #line_check($line, $onlycheck)
135             #$onlycheck: 0/undef: do full search for file/line titles and line end, etc.
136             # 1: only determine if data or not
137             sub line_check #return 1 and set separator and line ending if data line and 0 otherwise
138             {
139 92     92 1 198 my $self = shift;
140             # temporary hack until fully switching to value instead of ref, which is a ref anyway
141 92 100       295 my $lr = ref $_[0] ? $_[0] : \$_[0];
142 92         239 my $oc = $_[1];
143 92         224 my $zahl = $self->{config}{numregex};
144 92         223 my $seppl = $trenner;
145 92 100       297 $seppl = $self->{config}{separator} if $self->{config}{strict};
146             #the leading whitespace is a workaround for TISEAN
147             #good or bad? It should not break any files that worked before...
148 92 100 66     178 if( ${$lr} =~ /^\s*$/ and ${$lr} =~ /^($nlend*)($lend)$/o )
  92         584  
  2         26  
149             {
150 2 50       7 $self->{config}{lineend} = $2 unless defined $self->{config}{lineend};
151             # An empty line counts as comment when it comes after a title.
152 2         8 push(@{$self->{comments}},$1)
153 2 50       5 if defined $self->{title};
154 2         7 return 0;
155             }
156 90 100       214 if(${$lr} =~ /^\s*($zahl)(($seppl)$nlend*|)($lend)$/)
  90 50       2469  
157             {
158 38         237 my ($num, $end, $sep) = ($1, $4, $3);
159 38         156 my $piece = $1.$2;
160 38 100 66     284 unless(not defined $end or defined $self->{config}{lineend})
161             {
162 1         3 $self->{config}{lineend} = $end;
163             }
164 38 100       150 unless($self->{config}{text})
165             {
166             # If text is not allowed, we strictly only want
167             # numbers and separators and line end.
168             # Let's get expensive: Remove everything we know. if there is something
169             # left, we got text.
170 2         4 my $linecopy = ${$lr};
  2         7  
171 2         52 $linecopy =~ s/$seppl//g;
172 2         66 $linecopy =~ s/($zahl|\s+|$lend)//g;
173 2 100       10 if($linecopy ne '')
174             {
175 1 50       4 if( defined $self->{title} ){ push(@{$self->{comments}},$piece); }
  1         2  
  1         4  
176 0         0 else{ $self->{title} = $piece; }
177 1         4 return 0;
178             }
179             }
180             # sanity check for loosened definition of number... at least one digit shall be there
181 37 50       256 if($num =~ /\d/)
182             {
183 37 100 100     250 unless(not defined $sep or defined $self->{config}{separator})
184             {
185 30         125 $self->{config}{separator} = $sep;
186             }
187 37 100 66     85 if($#{$self->{comments}} > -1 and $#{$self->{titles}} > -1)
  37         205  
  20         116  
188             {
189 20         49 pop(@{$self->{comments}});
  20         63  
190             }
191 37         243 return 1; # Yeah, found a number line.
192             }
193             }
194 0         0 elsif($oc){ return 0; }
195             else
196             {
197 52 50       161 if(${$lr} =~ /^($self->{config}{comregex})($lend)$/)
  52         815  
198             {
199             $self->{config}{comchar} = $1
200 0 0       0 unless defined $self->{config}{comchar};
201             $self->{config}{lineend} = $2
202 0 0       0 unless defined $self->{config}{lineend};
203 0         0 return 0;
204             }
205             #first non-empty line is some kind of title or comment
206             #first means: we didn't have content up to now
207 52 50       153 if(${$lr} =~ /^($self->{config}{comregex})($nlend+)($lend)$/)
  52         811  
208             {
209 52 100       204 if( defined $self->{title} ){ push(@{$self->{comments}},$2); }
  30         64  
  30         151  
210 22         141 else{ $self->{title} = $2; }
211             $self->{config}{lineend} = $3
212 52 100       220 unless defined $self->{config}{lineend};
213             $self->{config}{comchar} = $1
214 52 100       213 unless defined $self->{config}{comchar};
215             }
216             #attention: I take " or ' just as quotes, do distinction!
217 52         152 my $quote = $self->{config}{quotechar};
218 52 50       168 $quote = $quotematch
219             unless defined $quote;
220 52 100       105 if(${$lr} =~ /^($self->{config}{comregex})($quote)($nlend*\2($seppl)\2*$nlend*)\2*($lend)$/)
  52 50       1396  
221             {
222             $self->{config}{quote} = 1
223 17 50       101 unless defined $self->{config}{quote};
224             $self->{config}{quotechar} = $2
225 17 50       108 unless defined $self->{config}{quotechar};
226             # "axis title"\t"axis title"\t"..."
227             # allow flexible space in separator
228 17         67 my $sep = $4;
229 17         58 my $q = $2;
230 17         53 my $rest = $3;
231 17         293 $rest =~ s:$q$::;
232 17 50       154 $sep =~ s:\s+$:\\s+:
233             unless($strict);
234 17         361 my @ax = split($q.$sep.$q,$rest);
235 17         92 $self->{titles} = \@ax;
236             $self->{config}{lineend} = $5
237 17 50       82 unless defined $self->{config}{lineend};
238             $self->{config}{comchar} = $1
239 17 50       88 unless defined $self->{config}{comchar};
240             }
241             #either no quotes at all or maybe quotes but single item without separator
242 35         781 elsif(${$lr} =~ /^($self->{config}{comregex})($quote?)($nlend*)($lend)$/)
243             {
244 35 50       181 if($2 ne '')
245             {
246             $self->{config}{quotechar} = $2
247 0 0       0 unless defined $self->{config}{quotechar};
248             $self->{config}{quote} = 1
249 0 0       0 unless defined $self->{config}{quote};
250             }
251             else
252             {
253 35         97 $self->{guessquote} = 0
254             }
255             $self->{config}{lineend} = $4
256 35 50       129 unless defined $self->{config}{lineend};
257             $self->{config}{comchar} = $1
258 35 50       121 unless defined $self->{config}{comchar};
259 35         111 my $d = $3;
260 35         252 $d =~ s/$quote$//;
261 35         102 my @ax = ();
262 35 100       760 if($d =~ /($seppl)/)
263             {
264 21         310 @ax = split($1, $d);
265             }
266 14         49 else{ @ax = ($d); }
267 35         165 $self->{titles} = \@ax;
268             }
269 52         355 return 0;
270             }
271             }
272              
273             sub get_insep
274             {
275 3     3 0 4 my $self = shift;
276             return defined $self->{config}{separator}
277             ? $self->{config}{separator}
278 3 50       23 : $default_sep;
279             }
280              
281             sub get_outsep
282             {
283 1912     1912 0 3404 my $self = shift;
284             return defined $self->{config}{outsep}
285             ? $self->{config}{outsep}
286             : (
287             defined $self->{config}{separator}
288             ? $self->{config}{separator}
289 1912 100       7083 : $default_sep
    100          
290             );
291             }
292              
293             sub get_end
294             {
295 2699     2699 0 4515 my $self = shift;
296             return defined $self->{config}{lineend}
297             ? $self->{config}{lineend}
298 2699 100       8483 : $default_eol;
299             }
300              
301             sub get_quote
302             {
303 21     21 0 42 my $self = shift;
304             my $want = defined $self->{config}{quote}
305             ? $self->{config}{quote}
306             : ( defined $self->{guessquote}
307             ? $self->{guessquote}
308 21 100       107 : $default_quote );
    100          
309             return $want
310             ? ( defined $self->{config}{quotechar}
311             ? $self->{config}{quotechar}
312 21 100       101 : $default_quotechar )
    100          
313             : '';
314             }
315              
316             sub get_comchar
317             {
318 805     805 0 1179 my $self = shift;
319             return defined $self->{config}{comchar}
320             ? $self->{config}{comchar}
321 805 100       1873 : $default_comchar;
322             }
323              
324             sub line_data
325             {
326 1440     1440 1 2683 my $self = shift;
327 1440 50       3761 my $lr = ref $_[0] ? $_[0] : \$_[0];
328 1440         3009 my @ar = ();
329 1440         3041 my $zahl = $self->{config}{numregex};
330             # empty lines
331 1440 0       2352 return ($self->{config}{empty} ? [] : undef) if(${$lr} =~ /^$lend$/);
  1440 50       7815  
332 1440 100       3895 if($self->{config}{strict})
333             {
334             #just split with defined or found separator
335 3         7 @ar = split($self->get_insep(), ${$lr});
  3         12  
336             #remove line end
337 3 50       9 if($#ar > -1){ $ar[$#ar] =~ s/$lend//o; }
  3         16  
338             }
339             else
340             {
341 1437         2390 my $l = ${$lr};
  1437         3593  
342 1437 100       3798 if($self->{config}{black})
343             {
344 3         13 $l =~ s/^\s*//;
345             # s/\s*$// deletes the line end -- no problem here
346 3         20 $l =~ s/\s*$//;
347             }
348 1437 50       9946 if($l =~ /^($zahl)(.*)$/){ push(@ar, $1); $l = $2; }else{ return undef; }
  1437         5448  
  1437         3579  
  0         0  
349 1437 100       11703 unless($self->{config}{text})
350             {
351 3         53 while($l =~ /^($trenner)($zahl)(.*)$/o)
352             {
353 6         11 push(@ar, $2);
354 6         22 $l = $3;
355             }
356             }
357             else
358             {
359 1434         6734 while($l =~ /^($trenner)($ntrenner)(.*)$/o)
360             {
361 1972         5512 push(@ar, $2);
362 1972         8362 $l = $3;
363             }
364             }
365             }
366 1440         5412 return \@ar;
367             }
368              
369             sub data_line
370             {
371 1891     1891 1 3655 my $self = shift;
372 1891         2962 my $ar = shift;
373              
374 1891         3156 my $wr = shift;
375 1891         3981 my $l = '';
376 1891         3661 my $zahl = $self->{config}{numregex};
377 1891         4015 my $end = $self->get_end();
378 1891         4697 my $sep = $self->get_outsep();
379 1891         3841 my @vals;
380             my @cols;
381 1891         2996 my $i = -1;
382              
383 1891 100       3788 unless(defined $wr)
384             {
385 1879         2831 @vals = @{$ar};
  1879         6365  
386 1879         4746 @cols = (0..$#vals);
387             }
388             else
389             {
390 12         31 for my $k (@{$wr})
  12         31  
391             {
392             push(@vals, ($k > -1 and $k < @{$ar})
393             ? $ar->[$k]
394 28 50 33     82 : $self->{config}{fill});
395 28 50       90 push(@cols, $k > -1 ? $k : 0); # ... for numerformat ... arrg
396             }
397             }
398              
399 1891 100       4755 if(defined $self->{config}{numformat}->[0])
400             {
401 1433         4136 foreach my $i (0..$#vals)
402             {
403 3582         8861 my $v = $vals[$i];
404 3582         6152 my $c = $cols[$i];
405 3582 50       8106 unless(defined $v){ $l .= $sep; next; }
  0         0  
  0         0  
406              
407 3582         7401 my $numform = $self->{config}{numformat}->[$c];
408 3582 100       8383 $numform = $self->{config}{numformat}->[0] unless defined $numform;
409 3582 50       7731 if($numform ne '')
410             {
411 3582   33     54992 $l .= ($v ne '' and $v =~ /^$zahl$/ ? sprintf($numform, $v) : $v).$sep;
412             }
413 0         0 else{ $l .= $v.$sep; }
414             }
415 1433         13711 $l =~ s/$sep$/$end/;
416             }
417             else
418             {
419             # do I want to care for undefs?
420             # not here ... failure is not communicated from here, you shall handle bad columns externally
421 458         2178 $l = join($sep, @vals).$end;
422             }
423 1891         11315 return \$l;
424             }
425              
426             sub title_line
427             {
428 21     21 1 49 my $self = shift;
429 21         45 my $ar = shift;
430              
431 21         53 my $end = $self->get_end();
432 21         74 my $sep = $self->get_outsep();
433 21         59 my $com = $self->get_comchar();
434 21         71 my $q = $self->get_quote();
435 21         61 my $l = $com.$q;
436             #print STDERR "titles: @{$self->{titles}}\n";
437             #print STDERR "titles for @{$ar}\n" if defined $ar;
438 21 100       70 unless(defined $ar){ $l = $com.$q.join($q.$sep.$q, @{$self->{titles}}).$q.$end; }
  18         54  
  18         124  
439             else
440             {
441 3         7 foreach my $k (@{$ar})
  3         9  
442             {
443             # should match for title containing $q
444 7 50       21 my $t = $k > -1 ? $self->{titles}->[$k] : undef;
445 7 50       22 $t = "" unless defined $t;
446 7         21 $l .= $t.$q.$sep.$q;
447             }
448 3         29 $l =~ s/$q$//;
449 3         29 $l =~ s/$sep$/$end/;
450             }
451 21         109 return \$l;
452            
453             }
454              
455             sub comment_line
456             {
457 784     784 1 1189 my $self = shift;
458 784 100       1630 my $line = ref $_[0] ? $_[0] : \$_[0];
459 784         1442 my $cline = $self->get_comchar().${$line}.$self->get_end();
  784         1891  
460 784         2966 return \$cline;
461             }
462              
463             sub chomp_line
464             {
465 0     0 1 0 my $self = shift;
466 0 0       0 my $string = ref $_[0] ? $_[0] : \$_[0];
467 0 0       0 if(defined $string)
468             {
469 0         0 ${$string} =~ s/$lend$//;
  0         0  
470             }
471             }
472              
473             sub make_naked
474             {
475 35     35 1 69 my $self = shift;
476 35 50       115 my $string = ref $_[0] ? $_[0] : \$_[0];
477 35 50       105 if(defined $string)
478             {
479 35         65 ${$string} =~ s/$lend$//;
  35         302  
480 35         84 ${$string} =~ s/^$self->{config}{comregex}//;
  35         335  
481             }
482             }
483              
484             # Not well supported, but possible: Text in between numeric data.
485             # To make it a bit safer, this filter will replace everything that would count as separator.
486             # It's only a bit safer... supsequent parsers are supposed to work in strict mode if we're in strict mode here.
487             sub filter_text
488             {
489 0     0 0   my $self = shift;
490 0           my $match;
491 0 0         if($self->{config}{strict})
492             {
493 0           my $sep = $self->get_outsep();
494 0           $match = qr/$sep/;
495             }
496             else
497             {
498 0           $match = qr/$trenner/;
499             }
500 0           for(@_){ s:$match:_:g; }
  0            
501             }
502              
503             1;
504              
505             __END__