File Coverage

blib/lib/Parse/FixedLength.pm
Criterion Covered Total %
statement 260 306 84.9
branch 120 178 67.4
condition 26 54 48.1
subroutine 29 36 80.5
pod 12 12 100.0
total 447 586 76.2


line stmt bran cond sub pod time code
1             package Parse::FixedLength;
2 16     16   30927 use strict;
  16         38  
  16         1160  
3              
4              
5             #-----------------------------------------------------------------------
6             # Public Global Variables
7             #-----------------------------------------------------------------------
8 16     16   89 use Carp;
  16         27  
  16         1938  
9 16     16   97 use vars qw($VERSION $DELIM $DEBUG);
  16         42  
  16         97309  
10             $VERSION = '5.39';
11             $DELIM = ":";
12             $DEBUG = 0;
13              
14             #=======================================================================
15             sub import {
16 21     21   148 my $proto = shift;
17 21   33     183 my $class = ref($proto) || $proto;
18 21         56795 for (@_) {
19 4 100       20 $class->import($class->_all_modules()), last if $_ eq ':all';
20 3     3   278 eval "use ${class}::$_";
  3         1909  
  3         725  
  3         41  
21 3 50       5017 confess $@ if $@;
22             }
23             }
24              
25             sub _all_modules {
26 1     1   3 my $self = shift;
27 1     1   76 eval "use File::Spec";
  1         9  
  1         2  
  1         16  
28 1 50       5 confess $@ if $@;
29 1         2 my %modules;
30 1         2 for my $dir (@INC) {
31 11         70 my $pfl_dir = File::Spec->catdir($dir, 'Parse', 'FixedLength');
32 11 100       284 next unless -d $pfl_dir;
33 1 50       49 opendir(DIR, $pfl_dir) or confess "Can't read $pfl_dir: $!";
34 1         46 for (readdir DIR) {
35 3         4 my $module = $_;
36 3 100       14 next unless $module =~ s/\.pm$//;
37 1         4 $modules{$module} = undef;
38             }
39 1         16 closedir DIR;
40             }
41 1         13 keys %modules;
42             }
43              
44             sub new {
45             # Do the cargo cult OO construction thing
46 27     27 1 8264 my $proto = shift;
47 27   33     186 my $class = ref($proto) || $proto;
48 27         54 my $format = shift;
49 27 100       137 unless (ref $format) {
50 1         4 my $newclass = "${class}::${format}";
51 1         2 my $result = eval { $newclass->new(@_)};
  1         20  
52 1 50       10 return $result unless $@;
53 1 50       7 confess $@ unless $@ =~ /Can't locate object method/;
54             # Assume we need to require this format
55 1         5 $class->import($format);
56 1         7 return $newclass->new(@_);
57             }
58 26 50       141 confess "Format argument not an array ref"
59             unless UNIVERSAL::isa($format, 'ARRAY');
60 26         87 my $self = bless {}, $class;
61 26   100     140 my $params = shift || {};
62 26 50 33     223 confess "Params argument not a hash ref"
63             if defined $params and ! UNIVERSAL::isa($params, 'HASH');
64 26 100       111 my $delim = exists $params->{'delim'} ? $params->{'delim'} : $DELIM;
65 26         147 $self->{DELIM} = $delim;
66 26         436 my $delim_re = qr/\Q$delim/;
67 26 50       110 confess "Delimiter argument must be one character" unless length($delim)==1;
68 26 100       104 if (exists $$params{all_lengths}) {
69 1         2 my $all = $$params{all_lengths};
70 1 50 33     15 confess "all_lengths must be a positive integer"
      33        
71             unless $all and $all =~ /^\d+$/ and $all > 0;
72 1         3 $format = [ map { local $_=$_; s/$delim_re.*//;
  4         7  
  4         25  
73 4         16 "${_}${delim}$$params{all_lengths}"
74             } @$format ];
75             }
76 26 50       90 my $spaces = $params->{'spaces'} ? 'a' : 'A';
77 26         92 my $is_hsh = $self->{IS_HSH} = _chk_format_type($format, $delim_re);
78              
79             # Convert hash-like array to delimited array
80 68         179 $format = [ map { $$format[$_].$delim.$$format[$_+1] }
  136         245  
81 26 100       118 grep { not $_ % 2 } 0..$#$format
82             ] if $is_hsh;
83 26         197 my ($names, $alengths, $hlengths, $justify, $length, $fmts) =
84             _parse_format($format, $delim_re, $params);
85              
86 22         56 $self->{NAMES} = $names;
87 22         48 $self->{UNPACK} = join '', @{$fmts}{@$names};
  22         124  
88 22         94 ( $self->{PACK} = $self->{UNPACK} ) =~ tr/a/A/;
89 22         59 $self->{LENGTH} = $length;
90 22         118 @$self{qw(TFIELDS TNAMES)} = ([], []);
91             # Save justify fields no matter what for benefit of dumper()
92 22 100       100 if (%$justify) {
93 13         29 $self->{JFIELDS} = $justify;
94 13 50       45 $self->{JUST} = 1 unless $$params{no_justify};
95 13         50 @$self{qw(TFIELDS TNAMES TPAD)} = _trim_info($self);
96 13 100       56 $self->{TRIM} = 1 if $$params{trim};
97             }
98 22         46 $self->{LENGTHS} = $hlengths;
99 22         63 $self->{FMTS} = $fmts;
100 22 0       105 $self->{DEBUG} = exists $$params{'debug'} ?
    50          
101             ref($$params{'debug'}) ? $$params{'debug'} : \*STDOUT : $DEBUG;
102              
103             # Make slot to parse data into
104 22   100     146 my $ref = $params->{href} || {};
105 22         49 @$ref{@{$self->names}} = undef;
  22         104  
106 22         57 $self->{DATA} = $ref;
107 22 50       182 $self->hash_to_obj($self->{DATA}) unless $params->{no_bless};
108              
109 22         147 $self;
110             }
111              
112             # Determine which format we have, the delimited array ref
113             # or the hash-like array ref.
114             # There must be delimiters in either all of the elements or none in
115             # alternating elements with an even number of elements.
116             # Assume what we have from the first element.
117             sub _chk_format_type {
118 26     26   52 my ($format, $delim) = @_;
119 26 100       152 my $is_hsh = 1 unless $$format[0] =~ $delim;
120 26 50 66     178 confess"Odd number of name/length pairs or missing delimiter on first field"
121             if $is_hsh and @$format % 2;
122 26         97 for my $i (0..$#$format) {
123 186         276 my $field = $$format[$i];
124 186 100       617 if ($field =~ $delim) {
125 98 50 66     425 confess "Field $field contains delimiter" if $is_hsh and not $i % 2;
126 88 50       204 } else { confess "Field $field is missing delimiter" unless $is_hsh }
127             }
128 26         99 return $is_hsh;
129             }
130              
131             sub _parse_format {
132 26     26   56 my ($format, $delim, $params) = @_;
133 26         50 my (@names, @lengths, %lengths, %justify, %dups, %fmts);
134 26         110 my $dups_ok = $$params{autonum};
135 26         45 my $all_dups_ok;
136 26 100       99 if ($dups_ok) {
137 6 100       24 if (UNIVERSAL::isa($dups_ok, 'ARRAY')) {
138 5         17 @dups{@$dups_ok} = undef;
139 1         2 } else { $all_dups_ok = 1 }
140             }
141 26         257 my $length = 0;
142 26         49 my $nxt = 1;
143 26         78 for (@$format) {
144 111         467 my ($name, $tmp_len, $start, $end) = split $delim;
145 111 100       431 _chk_start_end($name, $nxt, $start, $end) unless $$params{no_validate};
146 110         304 $name = _chk_dups(
147             $name, \@names, \%fmts,
148             \%lengths, \%justify, \%dups, $dups_ok, $all_dups_ok
149             );
150 108         200 push @names, $name;
151             # The results of the inner-parens is not guaranteed unless the
152             # outer parens match, so we do it this way
153 108 100       359 if ( $tmp_len =~ /^\d/ ) {
    50          
154 106 50       586 my ($len, $is_just, $chr) = $tmp_len =~ /^(\d+)((?:R(.?))?)$/
155             or confess "Bad length $tmp_len for field $name";
156 106 50       326 $len > 0 or confess "Length must be > 0 for field $name";
157 106 100       253 unless ( $$params{no_validate} ) {
158 100 100       201 if (defined $end) {
159 35 100       206 confess "Bad length or end for field $name"
160             unless $end == $start + $len - 1;
161             }
162             }
163 105 100       252 $justify{$name} = ($chr eq '') ? ' ' : $chr if $is_just;
    100          
164 105         271 $lengths{$name} = $len;
165 105         206 push @lengths, $len;
166 105         123 $length += $len;
167 105 100       255 $nxt = $end + 1 if defined $end;
168 105 50       440 $fmts{$name} = ( $params->{spaces} ? "a" : "A" ) . $len;
169             } elsif ( $tmp_len =~ /^(\w)((?:\d+)?)$/ ) {
170 2         7 my ($type, $repeat) = ($1, $2);
171 2   0     35 my $len = $type =~ /[AaZ]/ && $repeat
172             || $type =~ /b/i && int((($repeat/16)-.01) + 1)
173             || $type =~ /h/i && int((($repeat/2)-.01) + 1)
174             || $type =~ /c/i && 1
175             || $type =~ /[sSnv]/ && 2
176             || $type =~ /[lLNV]/ && 4
177             || $type =~ /q/i && 8
178             || undef;
179 2 50       5 unless ( $$params{no_validate} ) {
180 2 50       3 if (defined $end) {
181 2 50       6 confess "Bad length or end for field $name"
182             unless $end == $start + $len - 1;
183             }
184             }
185 2         2 $length += $len;
186 2         5 $lengths{$name} = $len;
187 2         1 push @lengths, $len;
188 2 50       5 $nxt = $end + 1 if defined $end;
189 2         5 $fmts{$name} = $tmp_len;
190             } else {
191 0 0       0 unless ( eval {
192 0         0 my @foo = unpack($tmp_len, "junk");
193 0 0       0 die "Too Many fields" unless @foo == 1;
194 0         0 1;
195             })
196             {
197 0         0 confess "Bad format $tmp_len for field $name";
198             }
199 0         0 $fmts{$name} = $tmp_len;
200             }
201             }
202 22         145 return \@names, \@lengths, \%lengths, \%justify, $length, \%fmts;
203             }
204              
205             # Check for duplicate field name, and if a duplicate,
206             # either die or return new autonumbered field name
207             sub _chk_dups {
208 110     110   192 my ($name, $names, $fmts, $lengths,
209             $justify, $dups, $dups_ok, $all_dups_ok) = @_;
210 110 100       233 if (exists $$lengths{$name}) {
211 8 100 66     424 confess "Duplicate field $name in format"
      66        
212             if !$dups_ok or !$all_dups_ok && !exists $$dups{$name};
213 102 100       371 } else { return $name unless $$dups{$name} }
214             # If this is the first duplicate found, fix the previous field
215 8 100       33 unless ($$dups{$name}) {
216 6         22 my $new_name = "${name}_".++$$dups{$name};
217 6 50       52 confess "Can't autonumber field $name" if exists $$lengths{$new_name};
218 6 100       15 for (@$names) { $_ = $new_name if $_ eq $name }
  18         56  
219 6         17 $$lengths{$new_name} = $$lengths{$name};
220 6         15 delete $$lengths{$name};
221 6         12 $$fmts{$new_name} = $$fmts{$name};
222 6         12 delete $$fmts{$name};
223 6 50       36 if (exists $$justify{$name}) {
224 0         0 $$justify{$new_name} = $$justify{$name};
225 0         0 delete $$justify{$name};
226             }
227             }
228 8         29 return "${name}_".++$$dups{$name};
229             }
230              
231             sub _chk_start_end {
232 105     105   208 my ($name, $prev, $start, $end) = @_;
233 105 100       300 if (defined $start) {
234 40 50       138 $start=~/^\d+$/ or confess "Start position not a number in field $name";
235 40 100       369 $start == $prev or confess "Bad start position in field $name";
236 39 50       65 defined $end or confess "End position missing in field $name";
237 39 50       120 $end =~ /^\d+$/ or confess "End position not a number in field $name";
238 39 50       102 $end < $start and confess "End position < start in field $name";
239             }
240             }
241              
242             sub _trim_info {
243 13     13   27 my $parser = shift;
244 13         19 my (@tfields, @tnames, @tpad);
245 13         16 my $i = 0;
246 13         25 for my $name (@{$parser->names}) {
  13         43  
247 61 100       153 if (exists $parser->{JFIELDS}{$name}) {
248 13         22 push @tfields, $i;
249 13         22 push @tnames, $name;
250 13         206 push @tpad, qr/^\Q$parser->{JFIELDS}{$name}\E+/;
251             }
252 61         91 } continue { $i++ }
253 13         60 return \@tfields, \@tnames, \@tpad;
254             }
255              
256             #=======================================================================
257             sub parse {
258 18     18 1 1955 my $parser = shift;
259 18         118 my $data = $parser->{DATA};
260 18         36 my $names = $parser->{NAMES};
261 18         122 @{$data}{@$names} = unpack($parser->{UNPACK}, $_[0]);
  18         701  
262 18 100       112 $parser->trim($data) if $parser->{TRIM};
263 18 50       646 if (my $fh = $parser->{DEBUG}) {
264 0         0 print $fh "# Debug parse\n";
265 0         0 for my $name (@$names) {
266 0         0 print $fh "[$name][$data->{$name}]\n";
267             }
268 0         0 print $fh "\n";
269             }
270 18 50       81 wantarray ? @$data{@$names} : $data;
271             }
272             #=======================================================================
273             sub parse_hash {
274 0     0 1 0 return %{ scalar(shift->parse(@_)) };
  0         0  
275             }
276             sub parse_newref {
277 0     0 1 0 return { shift->parse_hash(@_) };
278             }
279             #=======================================================================
280             sub pack {
281 7     7 1 52 my $parser = shift;
282 7   33     23 my $href = shift || $parser->{DATA};
283 7 50       38 if ($parser->{JUST}) {
284 7         10 while (my ($name, $chr) = each %{$parser->{JFIELDS}}) {
  14         56  
285 7         87 (my $field = $$href{$name}) =~ s/^\s+|\s+$//g;
286 7 100       80 $field =~ s/^${chr}+// if $chr ne ' ';
287 7         22 my $len = $parser->length($name);
288             # Should we warn if we're truncating the field?
289 7         38 $$href{$name} = substr(($chr x $len) . $field, -$len);
290             }
291             }
292             # Print debug output after justifying fields
293 7 50       23 if ($parser->{DEBUG}) {
294 0         0 print "# Debug pack\n";
295 0         0 for my $name (@{$parser->{NAMES}}) {
  0         0  
296 0         0 print "[$name][$$href{$name}]\n";
297             }
298 0         0 print "\n";
299             }
300 7         13 CORE::pack $parser->{PACK}, @$href{@{$parser->{NAMES}}};
  7         126  
301             }
302             #=======================================================================
303             sub trim {
304 1     1 1 2 my $self = shift;
305 1         1 my $i;
306 1 50       4 if (ref($_[0])) {
307 1         2 my $href = shift;
308 1         2 $href->{$_} =~ s/$self->{TPAD}[$i++]// for @{$self->{TNAMES}};
  1         23  
309             } else {
310 0         0 $_[$_] =~ s/$self->{TPAD}[$i++]// for @{$self->{TFIELDS}};
  0         0  
311             }
312             }
313             #=======================================================================
314 42     42 1 185 sub names { shift->{NAMES} }
315             #=======================================================================
316             sub length {
317 27     27 1 6179 my $self = shift;
318 27 100       776 @_ ? $self->{LENGTHS}{$_[0]} : $self->{LENGTH};
319             }
320             #=======================================================================
321             sub hash_to_obj {
322 22     22 1 68 my $self = shift;
323 22         40 my $href = shift;
324              
325 22         184 my $class_key = join "~=~", sort keys %$href;
326 22   66     150 my $class = $Parse::FixedLength::HashAsObj::classes{$class_key} || do {
327 16     16   161 no strict 'refs';
  16         27  
  16         14184  
328             my $name = $Parse::FixedLength::HashAsObj::classes{$class_key}
329             = "Parse::FixedLength::HashAsObj::Href" .
330             ++$Parse::FixedLength::HashAsObj::counter;
331             @{"${name}::ISA"} = "Parse::FixedLength::HashAsObj";
332             $name;
333             };
334 22         72 bless $href, $class;
335             }
336             #=======================================================================
337             sub dumper {
338 0     0 1 0 my $parser = shift;
339 0         0 my $pos_comment = shift;
340 0         0 my $start = 1;
341 0         0 my $end;
342 0         0 my $delim = $parser->{DELIM};
343             my $format = $pos_comment
344 0     0   0 ? sub { sprintf("%s => '%s', # %s-%s", @_) }
345             : $parser->{IS_HSH}
346 0     0   0 ? sub { sprintf("%s => '%s${delim}%s${delim}%s',", @_) }
347 0 0   0   0 : sub { join $delim, @_ };
  0 0       0  
348 0         0 my $layout = '';
349 0   0     0 my $jfields = $parser->{JFIELDS} || {};
350 0         0 for my $name (@{$parser->names}) {
  0         0  
351 0         0 my $len = $parser->length($name);
352 0         0 $end = $start + $len - 1;
353 0 0       0 my $just = exists $jfields->{$name}
    0          
354             ? $jfields->{$name} eq ' ' ? 'R' : "R$jfields->{$name}"
355             : '';
356 0         0 $len .= $just;
357 0         0 $layout .= $format->($name, $len, $start, $end) . "\n";
358 0         0 $start = $end + 1;
359             }
360 0         0 $layout;
361             }
362             #=======================================================================
363 0     0 1 0 sub format_str { shift->{UNPACK} }
364             #=======================================================================
365             sub converter {
366 6     6 1 118 Parse::FixedLength::Converter->new(@_);
367             }
368              
369             package Parse::FixedLength::Converter;
370 16     16   353 use Carp;
  16         34  
  16         12690  
371              
372             #=======================================================================
373             sub new {
374             # Do the OO cargo cult construction thing
375 6     6   14 my $proto = shift;
376 6   33     43 my $class = ref($proto) || $proto;
377 6         23 my $self = bless {}, $class;
378              
379 6         16 my ($parser1, $parser2, $mappings, $defaults, $parms) = @_;
380 6         47 $self->{UNPACKER} = $parser1;
381 6         20 $self->{PACKER} = $parser2;
382 6   50     20 $mappings ||= {};
383 6 50 33     79 confess 'Map arg not a hash or array ref'
384             unless UNIVERSAL::isa($mappings, 'ARRAY')
385             or UNIVERSAL::isa($mappings, 'HASH');
386 6 50       54 $self->{MAP} = { reverse UNIVERSAL::isa($mappings, 'HASH')
387             ? %$mappings : @$mappings
388             };
389 6   50     22 $defaults ||= {};
390 6 50       41 confess 'Defaults arg not a hash ref'
391             unless UNIVERSAL::isa($defaults, 'HASH');
392 6         14 my ($consts, $crefs) = ({}, {});
393 6         44 while (my ($field, $default) = each %$defaults) {
394 19 50 66     98 confess 'Default for field $field not a constant or code ref'
395             unless ! ref $default or UNIVERSAL::isa($default, 'CODE');
396 19 100       103 (ref $default ? $$crefs{$field} : $$consts{$field}) = $default;
397             }
398 6         13 $self->{CONSTANTS} = $consts;
399 6         15 $self->{CODEREFS} = $crefs;
400 6 100       21 $self->{NOPACK} = 1 if $parms->{no_pack};
401 6         23 $self;
402             }
403             #=======================================================================
404             sub convert {
405 7     7   1286 my $converter = shift;
406 7         15 my $data_in = shift;
407 7 100       675 my $no_pack = @_ ? shift : $converter->{NOPACK};
408 7         21 my $packer = $converter->{PACKER};
409 7         510 my $map_to = $converter->{MAP};
410              
411 7 50       596 $data_in = $converter->{UNPACKER}->parse($data_in)
412             unless UNIVERSAL::isa($data_in, 'HASH');
413 7         638 my $names_out = $packer->names;
414              
415             # Map the data from input to output
416 7         12 my $data_out = $packer->{DATA};
417 36 100       186 @$data_out{@$names_out} = map {
    100          
418 7         17 exists $map_to->{$_} ? $data_in->{$map_to->{$_}}
419             : exists $data_in->{$_} ? $data_in->{$_} : ''
420             } @$names_out;
421              
422             # Default/Convert the fields
423 7         18 while (my ($name, $default) = each %{$converter->{CONSTANTS}}) {
  14         58  
424 7         16 $data_out->{$name} = $default;
425             }
426 7         12 while (my ($name, $default) = each %{$converter->{CODEREFS}}) {
  22         75  
427 15         20 $data_out->{$name} = eval { $default->($data_out->{$name}, $data_in) };
  15         46  
428 15 50       95 confess "Failed to default field $name: $@" if $@;
429             }
430 7 100       33 $no_pack ? $data_out : $packer->pack($data_out);
431             }
432              
433             package Parse::FixedLength::HashAsObj;
434              
435 16     16   382 use vars qw($AUTOLOAD);
  16         33  
  16         1413  
436 22     22   5068 sub DESTROY { 1 }
437              
438             sub AUTOLOAD : lvalue {
439 16     16   77 no strict 'refs';
  16         24  
  16         3952  
440 5 50   5   658 my ( $class, $method ) = $AUTOLOAD =~ /^(.*)::(.+)$/
441             or Carp::croak "Invalid call to $AUTOLOAD";
442 5 50       30 Carp::croak "Can't locate object method $method via package $class"
443             unless exists $_[0]->{$method};
444             *$AUTOLOAD = sub : lvalue {
445 8     8   195 my $self = shift;
446 8 50       26 if (@_) {
447 0         0 $self->{$method} = shift;
448 0         0 return $self;
449             }
450 8         60 $self->{$method};
451 5         34 };
452 5         22 goto &$AUTOLOAD;
453             # To placate the compiler you must appear
454             # to return an lvalue-able value
455 0         0 $Parse::FixedLength::HashAsObj::foo;
456             }
457             1;
458             __END__