File Coverage

blib/lib/Text/xSV/Slurp.pm
Criterion Covered Total %
statement 197 214 92.0
branch 83 98 84.6
condition 17 20 85.0
subroutine 19 19 100.0
pod 1 1 100.0
total 317 352 90.0


line stmt bran cond sub pod time code
1             package Text::xSV::Slurp;
2              
3 9     9   294500 use warnings;
  9         25  
  9         309  
4 9     9   47 use strict;
  9         14  
  9         332  
5              
6 9     9   46 use Carp 'confess', 'cluck';
  9         27  
  9         996  
7 9     9   8832 use Text::CSV;
  9         156142  
  9         67  
8 9     9   8512 use IO::String;
  9         49857  
  9         476  
9              
10 9     9   86 use constant HOH_HANDLER_KEY => 0;
  9         19  
  9         1048  
11 9     9   46 use constant HOH_HANDLER_KEY_VALUE_PATH => 1;
  9         18  
  9         401  
12 9     9   130 use constant HOH_HANDLER_OLD_VALUE => 2;
  9         16  
  9         382  
13 9     9   46 use constant HOH_HANDLER_NEW_VALUE => 3;
  9         14  
  9         340  
14 9     9   123 use constant HOH_HANDLER_LINE_HASH => 4;
  9         19  
  9         346  
15 9     9   42 use constant HOH_HANDLER_HOH => 5;
  9         13  
  9         443  
16 9     9   58 use constant HOH_HANDLER_SCRATCH_PAD => 6;
  9         17  
  9         431  
17              
18 9     9   44 use base 'Exporter';
  9         14  
  9         39837  
19              
20             our @EXPORT = qw/ xsv_slurp /;
21              
22             =head1 NAME
23              
24             Text::xSV::Slurp - Convert xSV data to common data shapes.
25              
26             =head1 VERSION
27              
28             Version 0.22
29              
30             =cut
31              
32             our $VERSION = '0.22';
33              
34             =head1 SYNOPSIS
35              
36             C converts xSV (typically CSV) data to nested data structures
37             of various shapes. It allows both column and row filtering using user defined
38             functions.
39              
40             This brief example creates an array of hashes from a file, where each array
41             record corresponds to a line of the file, and each line is represented as a hash
42             of header-to-value pairs.
43              
44             use Text::xSV::Slurp 'xsv_slurp';
45            
46             my $aoh = xsv_slurp( 'foo.csv' );
47            
48             ## if foo.csv contains:
49             ##
50             ## uid,name
51             ## 342,tim
52             ## 939,danboo
53             ##
54             ## then $aoh contains:
55             ##
56             ## [
57             ## { uid => '342', name => 'tim' },
58             ## { uid => '939', name => 'danboo' },
59             ## ]
60            
61             =head1 FUNCTIONS
62              
63             =head2 C
64              
65             C converts xSV (typically CSV) data to nested data structures of
66             various shapes. It allows both column and row filtering using user defined
67             functions.
68              
69             Option summary:
70              
71             =over
72              
73             =item * C - file name to be opened
74              
75             =item * C - file handle to be iterated
76              
77             =item * C - string to be parsed
78              
79             =item * C - target data structure (C, C, C or C)
80              
81             =item * C - skip a subset of columns based on user callback
82              
83             =item * C - skip a subset of rows based on user callback
84              
85             =item * C - xSV string or ARRAY used to build the keys of the C shape
86              
87             =item * C - redefine how the C shape should store values
88              
89             =item * C - redefine how the C shape should handle key collisions
90              
91             =item * C - option hash for L/L constructor
92              
93             =back
94              
95             The C, C and C options are mutually exclusive. Only one
96             source parameter may be passed in each call to C, otherwise a fatal
97             exception will be raised.
98              
99             The source can also be provided implicitly, without the associated key, and the
100             source type will be guessed by examining the first item in the option list. If
101             the item is a reference type, it is treated as a C source. If the item
102             contains a newline or carriage return, it is treated as a C source. If
103             the item passes none of the prior tests, it is treated as a C source.
104              
105             ## implicit C source
106             my $aoa = xsv_slurp( \*STDIN, shape => 'aoa' );
107              
108             ## implicit C source
109             my $aoh = xsv_slurp( "h1,h2\n" . "d1,d2\n" );
110              
111             ## implicit C source
112             my $aoh = xsv_slurp( 'foo.csv' );
113              
114             The C parameter supports values of C, C, C or C. The
115             default shape is C. Each shape affects certain parameters differently (see
116             below).
117              
118             The C option can be used to control L/L
119             parsing. The given HASH reference is passed to the L constructor. If
120             the C option is undefined, the default L constructor is
121             called. For example, to change the separator to a colon, you could do the
122             following:
123              
124             my $aoh = xsv_slurp( file => 'foo.csv',
125             text_csv => { sep_char => ':' } );
126              
127             =head3 aoa
128              
129             =over
130              
131             example input:
132              
133             h1,h2,h3
134             l,m,n
135             p,q,r
136              
137              
138             example data structure:
139              
140             [
141             [ qw/ h1 h2 h3 / ],
142             [ qw/ l m n / ],
143             [ qw/ p q r / ],
144             ]
145              
146             shape specifics:
147              
148             =over
149              
150             =item * C - passed an ARRAY reference of indexes, should return a
151             list of indexes to be included
152              
153             =item * C - passed an ARRAY reference of values, should return true or
154             false whether the row should be included or not
155              
156             =back
157              
158             full example:
159              
160             ## - convert xSV example to an array of arrays
161             ## - include only rows containing values matching /[nr]/
162             ## - include only the first and last columns
163              
164             my $aoa = xsv_slurp( string => $xsv_data,
165             shape => 'aoa',
166             col_grep => sub { return @( shift() }[0,-1] },
167             row_grep => sub { return grep /[nr]/, @{ $_[0] } },
168             );
169              
170             ## $aoa contains:
171             ##
172             ## [
173             ## [ 'l', 'n' ],
174             ## [ 'p', 'r' ],
175             ## ]
176              
177             =back
178              
179             =head3 aoh
180              
181             =over
182              
183             example input:
184              
185             h1,h2,h3
186             l,m,n
187             p,q,r
188              
189             example data structure:
190              
191             [
192             { h1 => 'l', h2 => 'm', h3 => 'n' },
193             { h1 => 'p', h2 => 'q', h3 => 'r' },
194             ]
195              
196             shape specifics:
197              
198             =over
199              
200             =item * C - passed an ARRAY reference of column names, should return a
201             list of column names to be included
202              
203             =item * C - passed a HASH reference of column name / value pairs,
204             should return true or false whether the row should be
205             included or not
206              
207             =back
208              
209             full example:
210              
211             ## - convert xSV example to an array of hashes
212             ## - include only rows containing values matching /n/
213             ## - include only the h3 column
214              
215             my $aoh = xsv_slurp( string => $xsv_data,
216             shape => 'aoh',
217             col_grep => sub { return 'h3' },
218             row_grep => sub { return grep /n/, values %{ $_[0] } },
219             );
220              
221             ## $aoh contains:
222             ##
223             ## [
224             ## { h3 => 'n' },
225             ## ]
226              
227             =back
228              
229             =head3 hoa
230              
231             =over
232              
233             example input:
234              
235             h1,h2,h3
236             l,m,n
237             p,q,r
238              
239             example data structure:
240              
241             {
242             h1 => [ qw/ l p / ],
243             h2 => [ qw/ m q / ],
244             h3 => [ qw/ n r / ],
245             }
246              
247             shape specifics:
248              
249             =over
250              
251             =item * C - passed an ARRAY reference of column names, should return a
252             list of column names to be included
253              
254             =item * C - passed a HASH reference of column name / value pairs,
255             should return true or false whether the row should be
256             included or not
257              
258             =back
259              
260             full example:
261              
262             ## - convert xSV example to a hash of arrays
263             ## - include only rows containing values matching /n/
264             ## - include only the h3 column
265              
266             my $hoa = xsv_slurp( string => $xsv_data,
267             shape => 'hoa',
268             col_grep => sub { return 'h3' },
269             row_grep => sub { return grep /n/, values %{ $_[0] } },
270             );
271              
272             ## $hoa contains:
273             ##
274             ## {
275             ## h3 => [ qw/ n r / ],
276             ## }
277              
278             =back
279              
280             =head3 hoh
281              
282             =over
283              
284             example input:
285              
286             h1,h2,h3
287             l,m,n
288             p,q,r
289              
290             example data structure (assuming a C of C<'h2,h3'>):
291              
292             {
293             m => { n => { h1 => 'l' } },
294             q => { r => { h1 => 'p' } },
295             }
296              
297             shape specifics:
298              
299             =over
300              
301             =item * C - an xSV string or ARRAY specifying the indexing column names
302              
303             =item * C - passed an ARRAY reference of column names, should return a
304             list of column names to be included
305              
306             =item * C - passed a HASH reference of column name / value pairs,
307             should return true or false whether the row should be
308             included or not
309              
310             =item * C - specify how key collisions should be handled (see
311             L)
312              
313             =back
314              
315             full example:
316              
317             ## - convert xSV example to a hash of hashes
318             ## - index using h1 values
319             ## - include only rows containing values matching /n/
320             ## - include only the h3 column
321              
322             my $hoh = xsv_slurp( string => $xsv_data,
323             shape => 'hoh',
324             key => 'h1',
325             col_grep => sub { return 'h3' },
326             row_grep => sub { return grep /n/, values %{ $_[0] } },
327             );
328              
329             ## $hoh contains:
330             ##
331             ## {
332             ## l => { h3 => 'n' },
333             ## p => { h3 => 'r' },
334             ## }
335              
336             =back
337              
338             =head1 HoH storage handlers
339              
340             Using the C shape can result in non-unique C combinations. The default
341             action is to simply assign the values to the given slot as they are encountered,
342             resulting in any prior values being lost.
343              
344             For example, using C as the indexing key with the default collision
345             handler:
346              
347             $xsv_data = <
348             h1,h2,h3
349             1,2,3
350             1,2,5
351             EOXSV
352              
353             $hoh = xsv_slurp( string => $xsv_data,
354             shape => 'hoh',
355             key => 'h1,h2'
356             );
357            
358             would result in the initial value in the C

column being lost. The resulting

359             data structure would only record the C<5> value:
360              
361             {
362             1 => { 2 => { h3 => 5 } }, ## 3 sir!
363             }
364              
365             Typically this is not very useful. The user probably wanted to aggregate the
366             values in some way. This is where the C and C handlers
367             come in, allowing the caller to specify how these assignments should be
368             handled.
369              
370             The C handler is called for each assignment action, while the
371             C handler is only called when an actual collision occurs (i.e.,
372             the nested value path for the current line is the same as a prior line).
373              
374             If instead we wanted to push the values onto an array, we could use the built-in
375             C handler for the C event as follows:
376              
377             $hoh = xsv_slurp( string => $xsv_data,
378             shape => 'hoh',
379             key => 'h1,h2',
380             on_store => 'push',
381             );
382              
383             the resulting C, using the same data as above, would instead look like:
384              
385             {
386             1 => { 2 => { h3 => [3,5] } }, ## 3 sir!
387             }
388              
389             Or if we wanted to sum the values we could us the C handler for the
390             C event:
391              
392             $hoh = xsv_slurp( string => $xsv_data,
393             shape => 'hoh',
394             key => 'h1,h2',
395             on_collide => 'sum',
396             );
397            
398             resulting in the summation of the values:
399              
400             {
401             1 => { 2 => { h3 => 8 } },
402             }
403              
404             =head2 builtin C handlers
405              
406             A number of builtin C handlers are provided and can be specified
407             by name.
408              
409             The example data structures below use the following data.
410              
411             h1,h2,h3
412             1,2,3
413             1,2,5
414              
415             =head3 count
416              
417             Count the times a key occurs.
418              
419             { 1 => { 2 => { h3 => 2 } } }
420              
421             =head3 frequency
422              
423             Create a frequency count of values.
424              
425             { 1 => { 2 => { h3 => { 3 => 1, 5 => 1 } } } }
426              
427             =head3 push
428              
429             C values onto an array *always*.
430              
431             { 1 => { 2 => { h3 => [ 3, 5 ] } } }
432              
433             =head3 unshift
434              
435             C values onto an array *always*.
436              
437             { 1 => { 2 => { h3 => [ 5, 3 ] } } }
438              
439             =head2 builtin C handlers
440              
441             A number of builtin C handlers are provided and can be specified
442             by name.
443              
444             The example data structures below use the following data.
445              
446             h1,h2,h3
447             1,2,3
448             1,2,5
449              
450             =head3 sum
451              
452             Sum the values.
453              
454             { 1 => { 2 => { h3 => 8 } } }
455              
456             =head3 average
457              
458             Average the values.
459              
460             { 1 => { 2 => { h3 => 4 } } }
461              
462             =head3 push
463              
464             C values onto an array *only on colliding*.
465              
466             { 1 => { 2 => { h3 => [ 3, 5 ] } } }
467              
468             =head3 unshift
469              
470             C values onto an array *only on colliding*.
471              
472             { 1 => { 2 => { h3 => [ 5, 3 ] } } }
473              
474             =head3 die
475              
476             Carp::confess if a collision occurs.
477              
478             Error: key collision in HoH construction (key-value path was: { 'h1' => '1' }, { 'h2' => '2' })
479              
480             =head3 warn
481              
482             Carp::cluck if a collision occurs.
483              
484             Warning: key collision in HoH construction (key-value path was: { 'h1' => '1' }, { 'h2' => '2' })
485              
486             =cut
487              
488             my %shape_map =
489             (
490             'aoa' => \&_as_aoa,
491             'aoh' => \&_as_aoh,
492             'hoa' => \&_as_hoa,
493             'hoh' => \&_as_hoh,
494             );
495              
496             sub xsv_slurp
497             {
498 53     53 1 48836 my @o = @_;
499              
500             ## guess the source if there is an odd number of args
501 53 100       188 if ( @o % 2 )
502             {
503 1         3 my $src = shift @o;
504 1 50       11 if ( ref $src )
    50          
505             {
506 0         0 @o = ( handle => $src, @o );
507             }
508             elsif ( $src =~ /[\r\n]/ )
509             {
510 1         7 @o = ( string => $src, @o );
511             }
512             else
513             {
514 0         0 @o = ( file => $src, @o );
515             }
516             }
517              
518             ## convert argument list to option hash
519 53         184 my %o = @o;
520              
521             ## validate the source type
522 53         112 my @all_srcs = qw/ file handle string /;
523 53         114 my @given_srcs = grep { defined $o{$_} } @all_srcs;
  159         358  
524            
525 53 100       206 if ( ! @given_srcs )
    50          
526             {
527 2         295 confess "Error: no source given, specify one of: @all_srcs.";
528             }
529             elsif ( @given_srcs > 1 )
530             {
531 0         0 confess "Error: too many sources given (@given_srcs), specify only one.";
532             }
533              
534             ## validate the shape
535 51 100       197 my $shape = defined $o{'shape'} ? lc $o{'shape'} : 'aoh';
536 51         97 my $shaper = $shape_map{ $shape };
537            
538 51 100       151 if ( ! $shaper )
539             {
540 1         7 my @all_shapes = keys %shape_map;
541 1         348 confess "Error: unrecognized shape given ($shape). Must be one of: @all_shapes"
542             }
543            
544             ## check various global options for expectations
545 50 100 100     180 if ( defined $o{'col_grep'} && ref $o{'col_grep'} ne 'CODE' )
546             {
547 1         185 confess 'Error: col_grep must be a CODE ref';
548             }
549            
550 49 100 100     214 if ( defined $o{'row_grep'} && ref $o{'row_grep'} ne 'CODE' )
551             {
552 1         195 confess 'Error: row_grep must be a CODE ref';
553             }
554              
555             ## isolate the source
556 48         68 my $src = $given_srcs[0];
557            
558             ## convert the source to a handle
559 48         138 my $handle = _get_handle( $src => $o{$src} );
560            
561             ## create the CSV parser
562 47   66     455 my $csv = Text::CSV->new( $o{'text_csv'} || () );
563            
564             ## run the data conversion
565 47         3668 my $data = $shaper->( $handle, $csv, \%o );
566            
567 42         617 return $data;
568             }
569              
570             ## arguments:
571             ## $handle - file handle
572             ## $csv - the Text::CSV parser object
573             ## $o - the user options passed to xsv_slurp
574             sub _as_aoa
575             {
576 7     7   14 my ( $handle, $csv, $o ) = @_;
577            
578 7         14 my @aoa;
579              
580             my @cols;
581 0         0 my $col_grep;
582            
583 7         34 while ( my $line = $csv->getline($handle) )
584             {
585            
586             ## skip unwanted rows
587 14 100       35633 if ( defined $o->{'row_grep'} )
588             {
589 6 100       30 next if ! $o->{'row_grep'}->( $line );
590             }
591            
592             ## remove unwanted cols
593 10 100       53 if ( defined $o->{'col_grep'} )
594             {
595 3 100       8 if ( ! $col_grep )
596             {
597 1         3 $col_grep++;
598 1         3 @cols = $o->{'col_grep'}->( 0 .. $#{ $line } );
  1         10  
599             }
600 3         10 @{ $line } = @{ $line }[@cols];
  3         9  
  3         6  
601             }
602              
603 10         92 push @aoa, $line;
604            
605             }
606            
607 7 50       357 if ( ! $csv->eof )
608             {
609 0         0 confess 'Error: ' . $csv->error_diag;
610             }
611            
612 7         51 return \@aoa;
613             }
614            
615             ## arguments:
616             ## $handle - file handle
617             ## $csv - the Text::CSV parser object
618             ## $o - the user options passed to xsv_slurp
619             sub _as_aoh
620             {
621 9     9   18 my ( $handle, $csv, $o ) = @_;
622              
623 9         13 my @aoh;
624            
625 9         35 my $headers = $csv->getline($handle);
626            
627 9 100       10500 return \@aoh if $csv->eof;
628            
629 7 50       58 if ( ! defined $headers )
630             {
631 0         0 confess 'Error: ' . $csv->error_diag;
632             }
633            
634 7         14 my @headers = @{ $headers };
  7         23  
635              
636 7         13 my @grep_headers;
637            
638 7 100       239 if ( defined $o->{'col_grep'} )
639             {
640 1         7 @grep_headers = $o->{'col_grep'}->( @headers );
641             }
642            
643 7         40 while ( my $line = $csv->getline($handle) )
644             {
645            
646 7         1583 my %line;
647            
648 7         10 @line{ @headers } = @{ $line };
  7         49  
649              
650             ## skip unwanted rows
651 7 100       33 if ( defined $o->{'row_grep'} )
652             {
653 2 100       6 next if ! $o->{'row_grep'}->( \%line );
654             }
655              
656             ## remove unwanted cols
657 6 100       27 if ( defined $o->{'col_grep'} )
658             {
659 2         3 %line = map { $_ => $line{$_} } @grep_headers;
  4         16  
660             }
661            
662 6         33 push @aoh, \%line;
663            
664             }
665            
666 7 50       346 if ( ! $csv->eof )
667             {
668 0         0 confess 'Error: ' . $csv->error_diag;
669             }
670            
671 7         53 return \@aoh;
672             }
673              
674             ## arguments:
675             ## $handle - file handle
676             ## $csv - the Text::CSV parser object
677             ## $o - the user options passed to xsv_slurp
678             sub _as_hoa
679             {
680 6     6   13 my ( $handle, $csv, $o ) = @_;
681              
682 6         10 my %hoa;
683            
684 6         21 my $headers = $csv->getline($handle);
685            
686 6 100       1255 return \%hoa if $csv->eof;
687            
688 5 50       52 if ( ! defined $headers )
689             {
690 0         0 confess 'Error: ' . $csv->error_diag;
691             }
692            
693 5         9 my @headers = @{ $headers };
  5         17  
694            
695 5         12 my @grep_headers;
696            
697 5 100       28 if ( defined $o->{'col_grep'} )
698             {
699 1         6 @grep_headers = $o->{'col_grep'}->( @headers );
700 1         13 @hoa{ @grep_headers } = map { [] } @grep_headers;
  2         6  
701             }
702             else
703             {
704 4         12 @hoa{ @headers } = map { [] } @headers;
  11         53  
705             }
706            
707 5         86 while ( my $line = $csv->getline($handle) )
708             {
709 6         1262 my %line;
710            
711 6         25 @line{ @headers } = @{ $line };
  6         43  
712              
713             ## skip unwanted rows
714 6 100       24 if ( defined $o->{'row_grep'} )
715             {
716 2 100       8 next if ! $o->{'row_grep'}->( \%line );
717             }
718              
719             ## remove unwanted cols
720 5 100       21 if ( defined $o->{'col_grep'} )
721             {
722 2         4 %line = map { $_ => $line{$_} } @grep_headers;
  4         14  
723             }
724              
725 5         21 for my $k ( keys %line )
726             {
727 12         13 push @{ $hoa{$k} }, $line{$k};
  12         52  
728             }
729            
730             }
731            
732 5 50       256 if ( ! $csv->eof )
733             {
734 0         0 confess 'Error: ' . $csv->error_diag;
735             }
736            
737 5         42 return \%hoa;
738             }
739              
740             my %named_handlers =
741             (
742            
743             ## predefined methods for handling hoh storage
744             on_store =>
745             {
746              
747             ## count
748             'count' => sub
749             {
750             return ( $_[HOH_HANDLER_OLD_VALUE] || 0 ) + 1;
751             },
752              
753             ## value histogram (count occurences of each value)
754             'frequency' => sub
755             {
756             my $ref = $_[HOH_HANDLER_OLD_VALUE] || {};
757             $ref->{ $_[HOH_HANDLER_NEW_VALUE] } ++;
758             return $ref;
759             },
760            
761             ## push to array
762             'push' => sub
763             {
764             my $ref = $_[HOH_HANDLER_OLD_VALUE] || [];
765             push @{ $ref }, $_[HOH_HANDLER_NEW_VALUE];
766             return $ref;
767             },
768              
769             ## unshift to array
770             'unshift' => sub
771             {
772             my $ref = $_[HOH_HANDLER_OLD_VALUE] || [];
773             unshift @{ $ref }, $_[HOH_HANDLER_NEW_VALUE];
774             return $ref;
775             },
776            
777             },
778              
779             ## predefined methods for handling hoh collisions
780             on_collide =>
781             {
782            
783             ## sum
784             'sum' => sub
785             {
786             return ( $_[HOH_HANDLER_OLD_VALUE] || 0 ) + ( $_[HOH_HANDLER_NEW_VALUE] || 0 );
787             },
788              
789             ## average
790             'average' => sub
791             {
792             if ( ! exists $_[HOH_HANDLER_SCRATCH_PAD]{'count'} )
793             {
794             $_[HOH_HANDLER_SCRATCH_PAD]{'count'} = 1;
795             $_[HOH_HANDLER_SCRATCH_PAD]{'sum'} = $_[HOH_HANDLER_OLD_VALUE];
796             }
797             $_[HOH_HANDLER_SCRATCH_PAD]{'count'}++;
798             $_[HOH_HANDLER_SCRATCH_PAD]{'sum'} += $_[HOH_HANDLER_NEW_VALUE];
799             return $_[HOH_HANDLER_SCRATCH_PAD]{'sum'} / $_[HOH_HANDLER_SCRATCH_PAD]{'count'};
800             },
801              
802             ## die
803             'die' => sub
804             {
805             if ( defined $_[HOH_HANDLER_OLD_VALUE] )
806             {
807             my @kv_pairs = @{ $_[HOH_HANDLER_KEY_VALUE_PATH] };
808             my @kv_strings = map { "{ '$_->[0]' => '$_->[1]' }" } @kv_pairs;
809             my $kv_path = join ', ', @kv_strings;
810             confess "Error: key collision in HoH construction (key-value path was: $kv_path)";
811             }
812             },
813              
814             ## warn
815             'warn' => sub
816             {
817             if ( defined $_[HOH_HANDLER_OLD_VALUE] )
818             {
819             my @kv_pairs = @{ $_[HOH_HANDLER_KEY_VALUE_PATH] };
820             my @kv_strings = map { "{ '$_->[0]' => '$_->[1]' }" } @kv_pairs;
821             my $kv_path = join ', ', @kv_strings;
822             cluck "Warning: key collision in HoH construction (key-value path was: $kv_path)";
823             }
824             return $_[HOH_HANDLER_NEW_VALUE];
825             },
826              
827             ## push to array
828             'push' => sub
829             {
830             my $ref = ref $_[HOH_HANDLER_OLD_VALUE]
831             ? $_[HOH_HANDLER_OLD_VALUE]
832             : [ $_[HOH_HANDLER_OLD_VALUE] ];
833             push @{ $ref }, $_[HOH_HANDLER_NEW_VALUE];
834             return $ref;
835             },
836              
837             ## unshift to array
838             'unshift' => sub
839             {
840             my $ref = ref $_[HOH_HANDLER_OLD_VALUE]
841             ? $_[HOH_HANDLER_OLD_VALUE]
842             : [ $_[HOH_HANDLER_OLD_VALUE] ];
843             unshift @{ $ref }, $_[HOH_HANDLER_NEW_VALUE];
844             return $ref;
845             },
846            
847             },
848              
849             );
850              
851             ## arguments:
852             ## $handle - file handle
853             ## $csv - the Text::CSV parser object
854             ## $o - the user options passed to xsv_slurp
855             sub _as_hoh
856             {
857 25     25   42 my ( $handle, $csv, $o ) = @_;
858              
859 25         34 my %hoh;
860            
861 25         98 my $headers = $csv->getline($handle);
862            
863 25 100       38877 return \%hoh if $csv->eof;
864            
865 24 50       168 if ( ! defined $headers )
866             {
867 0         0 confess 'Error: ' . $csv->error_diag;
868             }
869            
870 24         35 my @headers = @{ $headers };
  24         71  
871            
872 24         41 my @grep_headers;
873            
874 24 100       82 if ( defined $o->{'col_grep'} )
875             {
876 1         4 @grep_headers = $o->{'col_grep'}->( @headers );
877             }
878              
879 24         40 my @key;
880            
881 24 50       112 if ( ref $o->{'key'} )
    100          
882             {
883            
884 0         0 @key = @{ $o->{'key'} };
  0         0  
885            
886             }
887             elsif ( defined $o->{'key'} )
888             {
889            
890 23 50       79 if ( ! $csv->parse( $o->{'key'} ) )
891             {
892 0         0 confess 'Error: ' . $csv->error_diag;
893             }
894            
895 23         2422 @key = $csv->fields;
896              
897             }
898             else
899             {
900 1         104 confess 'Error: no key given for hoh shape';
901             }
902              
903             ## set the on_collide handler at the default level and by header
904 23         152 my %storage_handlers;
905              
906 23         43 for my $header ( @headers )
907             {
908            
909 66         94 for my $type ( qw/ on_store on_collide / )
910             {
911            
912 131         162 my $handler = $o->{$type};
913              
914 131 100       286 next if ! $handler;
915            
916 51 100       96 if ( ref $handler eq 'HASH' )
917             {
918 14         25 $handler = $handler->{$header};
919             }
920            
921 51 100       87 next if ! $handler;
922              
923 47 100       83 if ( ! ref $handler )
924             {
925              
926 44 100       110 if ( ! exists $named_handlers{$type}{$handler} )
927             {
928 2         2 my $all_names = join ', ', sort keys %{ $named_handlers{$type} };
  2         15  
929 2         310 confess "Error: invalid '$type' handler given ($handler). Must be one of: $all_names."
930             }
931              
932 42         67 $handler = $named_handlers{$type}{$handler};
933             }
934            
935 45 100       199 confess "Error: cannot set multiple storage handlers for '$header'"
936             if $storage_handlers{$header};
937              
938 44         165 $storage_handlers{$header}{$type} = $handler;
939            
940             }
941              
942             }
943            
944             ## per-header scratch-pads used in collision functions
945 20         53 my %scratch_pads = map { $_ => {} } @headers;
  63         182  
946              
947 20         87 while ( my $line = $csv->getline($handle) )
948             {
949            
950 55         12197 my %line;
951            
952 55         89 @line{ @headers } = @{ $line };
  55         234  
953            
954             ## skip unwanted rows
955 55 100       171 if ( defined $o->{'row_grep'} )
956             {
957 2 100       20 next if ! $o->{'row_grep'}->( \%line );
958             }
959              
960             ## step through the nested keys
961 54         74 my $leaf = \%hoh;
962            
963 54         75 my @val;
964            
965 54         78 for my $k ( @key )
966             {
967            
968 101         131 my $v = $line{$k};
969 101   100     345 $leaf->{$v} ||= {};
970 101         468 $leaf = $leaf->{$v};
971            
972 101         229 push @val, $v;
973            
974             }
975            
976             ## remove key headers from the line
977 54         131 delete @line{ @key };
978            
979             ## remove unwanted cols
980 54 100       124 if ( defined $o->{'col_grep'} )
981             {
982 2         4 %line = map { $_ => $line{$_} } @grep_headers;
  2         10  
983             }
984              
985             ## perform the aggregation if applicable
986 54         114 for my $key ( keys %line )
987             {
988              
989 73         106 my $new_value = $line{$key};
990              
991 73         119 my $on_collide = $storage_handlers{$key}{'on_collide'};
992 73         96 my $on_store = $storage_handlers{$key}{'on_store'};
993            
994 73 100 100     398 if ( $on_store || $on_collide && exists $leaf->{$key} )
      66        
995             {
996            
997 47   66     142 my $handler = $on_collide || $on_store;
998              
999 47         374 $new_value = $handler->(
1000             $key, ## HOH_HANDLER_KEY
1001             [ map [ $key[$_] => $val[$_] ], 0 .. $#key ], ## HOH_HANDLER_KEY_VALUE_PATH
1002             $leaf->{$key}, ## HOH_HANDLER_OLD_VALUE
1003             $new_value, ## HOH_HANDLER_NEW_VALUE
1004             \%line, ## HOH_HANDLER_LINE_HASH
1005             \%hoh, ## HOH_HANDLER_HOH
1006             $scratch_pads{$key}, ## HOH_HANDLER_SCRATCH_PAD
1007             );
1008              
1009             }
1010              
1011 72         650 $leaf->{$key} = $new_value;
1012              
1013             }
1014            
1015             }
1016            
1017 19 50       1071 if ( ! $csv->eof )
1018             {
1019 0         0 confess 'Error: ' . $csv->error_diag;
1020             }
1021            
1022 19         199 return \%hoh;
1023             }
1024              
1025             ## arguments:
1026             ## $src_type - type of data source, handle, string or file
1027             ## $src_value - the file name, file handle or xSV string
1028             sub _get_handle
1029             {
1030 48     48   211 my ( $src_type, $src_value ) = @_;
1031              
1032 48 50       135 if ( $src_type eq 'handle' )
1033             {
1034 0         0 return $src_value;
1035             }
1036              
1037 48 100       116 if ( $src_type eq 'string' )
1038             {
1039 47         309 my $handle = IO::String->new( $src_value );
1040 47         2220 return $handle;
1041             }
1042              
1043 1 50       4 if ( $src_type eq 'file' )
1044             {
1045 1 50       214 open( my $handle, '<', $src_value ) || confess "Error: could not open '$src_value': $!";
1046 0           return $handle;
1047             }
1048              
1049 0           confess "Error: could not determine source type";
1050             }
1051              
1052             =head1 AUTHOR
1053              
1054             Dan Boorstein, C<< >>
1055              
1056             =head1 TODO
1057              
1058             =over
1059              
1060             =item * add xsv_eruct() to dump shapes to xsv data
1061              
1062             =item * add weighted-average collide keys and tests
1063              
1064             =item * document hoh 'on_store/on_collide' custom keys
1065              
1066             =item * add a recipes/examples section to cover grep and on_collide examples
1067              
1068             =back
1069              
1070             =head1 BUGS
1071              
1072             Please report any bugs or feature requests to C, or through
1073             the web interface at L. I will be notified, and then you'll
1074             automatically be notified of progress on your bug as I make changes.
1075              
1076              
1077             =head1 SUPPORT
1078              
1079             You can find documentation for this module with the perldoc command.
1080              
1081             perldoc Text::xSV::Slurp
1082              
1083              
1084             You can also look for information at:
1085              
1086             =over 4
1087              
1088             =item * RT: CPAN's request tracker
1089              
1090             L
1091              
1092             =item * AnnoCPAN: Annotated CPAN documentation
1093              
1094             L
1095              
1096             =item * CPAN Ratings
1097              
1098             L
1099              
1100             =item * Search CPAN
1101              
1102             L
1103              
1104             =back
1105              
1106              
1107             =head1 ACKNOWLEDGEMENTS
1108              
1109              
1110             =head1 COPYRIGHT & LICENSE
1111              
1112             Copyright 2009 Dan Boorstein.
1113              
1114             This program is free software; you can redistribute it and/or modify it
1115             under the terms of either: the GNU General Public License as published
1116             by the Free Software Foundation; or the Artistic License.
1117              
1118             See http://dev.perl.org/licenses/ for more information.
1119              
1120              
1121             =cut
1122              
1123             1; # End of Text::xSV::Slurp