File Coverage

blib/lib/Text/NumericData.pm
Criterion Covered Total %
statement 214 255 83.9
branch 108 160 67.5
condition 17 24 70.8
subroutine 15 17 88.2
pod 7 15 46.6
total 361 471 76.6


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