File Coverage

lib/Parse/Taxonomy/AdjacentList.pm
Criterion Covered Total %
statement 258 258 100.0
branch 103 108 95.3
condition 36 36 100.0
subroutine 21 21 100.0
pod 6 9 66.6
total 424 432 98.1


line stmt bran cond sub pod time code
1             package Parse::Taxonomy::AdjacentList;
2 4     4   6728 use strict;
  4         4  
  4         116  
3 4     4   1231 use parent qw( Parse::Taxonomy );
  4         791  
  4         17  
4 4     4   162 use Carp;
  4         6  
  4         178  
5 4     4   2346 use Text::CSV_XS;
  4         27062  
  4         189  
6 4     4   23 use Scalar::Util qw( reftype );
  4         5  
  4         168  
7 4     4   15 use Cwd;
  4         34  
  4         267  
8             our $VERSION = '0.24';
9 4         9151 use Parse::Taxonomy::Auxiliary qw(
10             path_check_fields
11             components_check_fields
12 4     4   996 );
  4         6  
13              
14             =head1 NAME
15              
16             Parse::Taxonomy::AdjacentList - Extract a taxonomy from a hierarchy inside a CSV file
17              
18             =head1 SYNOPSIS
19              
20             use Parse::Taxonomy::AdjacentList;
21              
22             $source = "./t/data/alpha.csv";
23             $self = Parse::Taxonomy::AdjacentList->new( {
24             file => $source,
25             } );
26              
27             =cut
28              
29             =head1 METHODS
30              
31             =head2 C
32              
33             =over 4
34              
35             =item * Purpose
36              
37             Parse::Taxonomy::AdjacentList constructor.
38              
39             =item * Arguments
40              
41             Single hash reference. There are two possible interfaces: C and C.
42              
43             =over 4
44              
45             =item 1 C interface
46              
47             $source = "./t/data/delta.csv";
48             $self = Parse::Taxonomy::AdjacentList->new( {
49             file => $source,
50             } );
51              
52             Elements in the hash reference are keyed on:
53              
54             =over 4
55              
56             =item * C
57              
58             Absolute or relative path to the incoming taxonomy file.
59             B for this interface.
60              
61             =item * C
62              
63             The name of the column in the header row under which each data record's unique
64             ID can be found. Defaults to C.
65              
66             =item * C
67              
68             The name of the column in the header row under which each data record's parent
69             ID can be found. (Will be empty in the case of top-level nodes, as they have
70             no parent.) Defaults to C.
71              
72             =item * C
73              
74             The name of the column in the header row under which, in each data record, there
75             is a found a string which differentiates that record from all other records
76             with the same parent ID. Defaults to C.
77              
78             =item * Text::CSV_XS options
79              
80             Any other options which could normally be passed to Cnew()> will
81             be passed through to that module's constructor. On the recommendation of the
82             Text::CSV documentation, C is always set to a true value.
83              
84             =back
85              
86             =item 2 C interface
87              
88             $self = Parse::Taxonomy::AdjacentList->new( {
89             components => {
90             fields => $fields,
91             data_records => $data_records,
92             }
93             } );
94              
95             Elements in this hash are keyed on:
96              
97             =over 4
98              
99             =item * C
100              
101             This element is B for the C interface. The value of this
102             element is a hash reference with two keys, C and C.
103             C is a reference to an array holding the field or column names for the
104             data set. C is a reference to an array of array references,
105             each of the latter arrayrefs holding one record or row from the data set.
106              
107             =back
108              
109             =back
110              
111             =item * Return Value
112              
113             Parse::Taxonomy::AdjacentList object.
114              
115             =item * Exceptions
116              
117             C will throw an exception under any of the following conditions:
118              
119             =over 4
120              
121             =item * Argument to C is not a reference.
122              
123             =item * Argument to C is not a hash reference.
124              
125             =item * Argument to C must have either 'file' or 'components' element but not both.
126              
127             =item * Lack columns in header row to match requirements.
128              
129             =item * Non-numeric entry in C or C column.
130              
131             =item * Duplicate entries in C column.
132              
133             =item * Number of fields in a data record does not match number in header row.
134              
135             =item * Empty string in a C column of a record.
136              
137             =item * Unable to locate a record whose C is the C of a different record.
138              
139             =item * No records with same C may share value of C column.
140              
141             =item * C interface
142              
143             =over 4
144              
145             =item * In the C interface, unable to locate the file which is the value of the C element.
146              
147             =item * The same field is found more than once in the header row of the
148             incoming taxonomy file.
149              
150             =item * Unable to open or close the incoming taxonomy file for reading.
151              
152             =back
153              
154             =item * C interface
155              
156             =over 4
157              
158             =item * In the C interface, C element must be a hash reference with C and C elements.
159              
160             =item * C element must be array reference.
161              
162             =item * C element must be reference to array of array references.
163              
164             =item * No duplicate fields in C element's array reference.
165              
166             =back
167              
168             =back
169              
170             =back
171              
172             =cut
173              
174             sub new {
175 52     52 1 40090 my ($class, $args) = @_;
176 52         55 my $data;
177              
178 52 100 100     612 croak "Argument to 'new()' must be hashref"
179             unless (ref($args) and reftype($args) eq 'HASH');
180 50         44 my $argscount = 0;
181 50 100       101 $argscount++ if $args->{file};
182 50 100       109 $argscount++ if $args->{components};
183 50 100       157 croak "Argument to 'new()' must have either 'file' or 'components' element"
184             if ($argscount == 0);
185 49 100       169 croak "Argument to 'new()' must have either 'file' or 'components' element but not both"
186             if ($argscount == 2);
187              
188             $data->{id_col} = $args->{id_col}
189             ? delete $args->{id_col}
190 48 100       124 : 'id';
191             $data->{parent_id_col} = $args->{parent_id_col}
192             ? delete $args->{parent_id_col}
193 48 100       93 : 'parent_id';
194             $data->{leaf_col} = $args->{leaf_col}
195             ? delete $args->{leaf_col}
196 48 100       72 : 'name';
197              
198 48 100       93 if ($args->{components}) {
199             croak "Value of 'components' element must be hashref"
200 22 100 100     264 unless (ref($args->{components}) and reftype($args->{components}) eq 'HASH');
201 20         31 for my $k ( qw| fields data_records | ) {
202             croak "Value of 'components' element must have '$k' key-value pair"
203 37 100       216 unless exists $args->{components}->{$k};
204             croak "Value of '$k' element must be arrayref"
205             unless (ref($args->{components}->{$k}) and
206 35 100 100     400 reftype($args->{components}->{$k}) eq 'ARRAY');
207             }
208 15         14 for my $row (@{$args->{components}->{data_records}}) {
  15         29  
209 173 100 100     693 croak "Each element in 'data_records' array must be arrayref"
210             unless (ref($row) and reftype($row) eq 'ARRAY');
211             }
212 13         28 _prepare_fields($data, $args->{components}->{fields}, 1);
213 9         12 my $these_data_records = $args->{components}->{data_records};
214 9         13 delete $args->{components};
215 9         19 _prepare_data_records($data, $these_data_records, $args);
216             }
217             else {
218             croak "Cannot locate file '$args->{file}'"
219 26 100       504 unless (-f $args->{file});
220 25         66 $data->{file} = delete $args->{file};
221 25         34 $args->{binary} = 1;
222 25 50       105 my $csv = Text::CSV_XS->new ( $args )
223             or croak "Cannot use CSV: ".Text::CSV_XS->error_diag ();
224             open my $IN, "<", $data->{file}
225 25 50       2310 or croak "Unable to open '$data->{file}' for reading";
226 25     4   876 my $header_ref = $csv->getline($IN);
  4         1719  
  4         16666  
  4         99  
227 25         844 _prepare_fields($data, $header_ref);
228              
229 21         441 my $data_records = $csv->getline_all($IN);
230 21 50       5890 close $IN or croak "Unable to close after reading";
231 21         48 _prepare_data_records($data, $data_records, $args);
232             }
233              
234 19         26 while (my ($k,$v) = each %{$args}) {
  33         87  
235 14         26 $data->{$k} = $v;
236             }
237 19         114 return bless $data, $class;
238             }
239              
240             sub _prepare_fields {
241 38     38   50 my ($data, $fields_ref, $components) = @_;
242 38 100       64 if (! $components) {
243 25         57 path_check_fields($data, $fields_ref);
244 24         45 _check_required_columns($data, $fields_ref);
245             }
246             else { # 'components' interface
247 13         50 components_check_fields($data, $fields_ref);
248 12         18 _check_required_columns($data, $fields_ref);
249             }
250 30         36 $data->{fields} = $fields_ref;
251 30         27 return $data;
252             }
253              
254             sub _check_required_columns {
255 36     36   41 my ($data, $fields_ref) = @_;
256 36         44 my %col2idx = map { $fields_ref->[$_] => $_ } (0 .. $#{$fields_ref});
  259         383  
  36         64  
257 36         69 my %missing_columns = ();
258 36         38 my %main_columns = map { $_ => 1 } ( qw| id_col parent_id_col leaf_col | );
  108         143  
259 36         84 for my $c ( keys %main_columns ) {
260 108 100       217 if (! exists $col2idx{$data->{$c}}) {
261 6         9 $missing_columns{$c} = $data->{$c};
262             }
263             }
264 36         43 my $error_msg = "Could not locate columns in header to match required arguments:";
265 36         70 for my $c (sort keys %missing_columns) {
266 6         11 $error_msg .= "\n $c: $missing_columns{$c}";
267             }
268 36 100       647 croak $error_msg if scalar keys %missing_columns;
269 30         39 $data->{fields} = $fields_ref;
270 30         45 for my $c (keys %main_columns) {
271 90         178 $data->{$c.'_idx'} = $col2idx{$data->{$c}};
272             }
273 30         93 return $data;
274             }
275              
276             sub _prepare_data_records {
277 30     30   34 my ($data, $data_records, $args) = @_;
278             # Confirm no duplicate entries in 'id_col'. DONE
279             # Confirm all rows have same number of columns as header. DONE
280 30         31 my $error_msg = '';
281 30         25 my $field_count = scalar(@{$data->{fields}});
  30         46  
282 30         37 my @non_numeric_id_records = ();
283 30         41 my %ids_seen = ();
284 30         30 my @bad_count_records = ();
285 30         27 my @nameless_component_records = ();
286 30         27 for my $rec (@{$data_records}) {
  30         40  
287 382 100       849 if ($rec->[$data->{id_col_idx}] !~ m/^\d+$/) {
288 2         3 push @non_numeric_id_records, [ $rec->[$data->{id_col_idx}], '' ];
289             }
290 382 100 100     1191 if (length($rec->[$data->{parent_id_col_idx}]) and
291             $rec->[$data->{parent_id_col_idx}] !~ m/^\d+$/
292             ) {
293             push @non_numeric_id_records, [
294             $rec->[$data->{id_col_idx}],
295 2         5 $rec->[$data->{parent_id_col_idx}]
296             ];
297             }
298 382         497 $ids_seen{$rec->[$data->{id_col_idx}]}++;
299 382         229 my $this_row_count = scalar(@{$rec});
  382         317  
300 382 100       480 if ($this_row_count != $field_count) {
301             push @bad_count_records,
302 6         9 [ $rec->[$data->{id_col_idx}], $this_row_count ];
303             }
304 382 100       600 if (! length($rec->[$data->{leaf_col_idx}])) {
305 3         4 push @nameless_component_records, $rec->[$data->{id_col_idx}];
306             }
307             }
308 30         76 $error_msg = <
309             Non-numeric entries are not permitted in the '$data->{id_col}' or '$data->{parent_id_col}' columns.
310             The following records each violate this restriction one or two times:
311             NON_NUMERIC_IDS
312 30         40 for my $rec (@non_numeric_id_records) {
313 4         8 $error_msg .= " $data->{id_col}: $rec->[0]\t$data->{parent_id_col}: $rec->[1]\n";
314             }
315 30 100       155 croak $error_msg if @non_numeric_id_records;
316              
317 29         36 my @dupe_ids = ();
318 29         158 for my $id (sort keys %ids_seen) {
319 365 100       478 push @dupe_ids, $id if $ids_seen{$id} > 1;
320             }
321 29         68 $error_msg = <
322             No duplicate entries are permitted in the '$data->{id_col}'column.
323             The following entries appear the number of times shown:
324             ERROR_MSG_DUPE
325 29         37 for my $id (@dupe_ids) {
326 4         12 $error_msg .= " $id:" . sprintf(" %6s\n" => $ids_seen{$id});
327             }
328 29 100       226 croak $error_msg if @dupe_ids;
329              
330 27         67 $error_msg = <
331             Header row has $field_count columns. The following records
332             (identified by the value in their '$data->{id_col}' columns)
333             have different counts:
334             ERROR_MSG_WRONG_COUNT
335 27         30 for my $rec (@bad_count_records) {
336 6         10 $error_msg .= " $rec->[0]: $rec->[1]\n";
337             }
338 27 100       251 croak $error_msg if @bad_count_records;
339              
340 25         43 $error_msg = <
341             Each data record must have a non-empty string in its 'leaf' column.
342             The following records (identified by the value in their '$data->{id_col}' columns)
343             lack valid leaves:
344             NAMELESS_LEAF
345 25         30 for my $rec (@nameless_component_records) {
346 3         4 $error_msg .= " id: $rec\n";
347             }
348 25 100       132 croak $error_msg if @nameless_component_records;
349              
350 24         33 my %ids_missing_parents = ();
351 24         17 for my $rec (@{$data_records}) {
  24         31  
352 304         249 my $parent_id = $rec->[$data->{parent_id_col_idx}];
353 304 100 100     759 if ( (length($parent_id)) and (! $ids_seen{$parent_id}) ) {
354 4         8 $ids_missing_parents{$rec->[$data->{id_col_idx}]} = $parent_id;
355             }
356             }
357 24         66 $error_msg = <
358             For each record with a non-null value in the '$data->{parent_id_col}' column,
359             there must be a record with that value in the '$data->{id_col}' column.
360             The following records (identified by the value in their '$data->{id_col}' columns)
361             appear to to have parent IDs which do not have records of their own:
362             ERROR_MSG_MISSING_PARENT
363 24         48 for my $k (sort {$a <=> $b} keys %ids_missing_parents) {
  2         7  
364 4         7 $error_msg .= " $k: $ids_missing_parents{$k}\n";
365             }
366 24 100       276 croak $error_msg if scalar keys %ids_missing_parents;
367              
368 22         26 my %families = ();
369 22         16 for my $rec (@{$data_records}) {
  22         31  
370 278 100       355 if (length($rec->[$data->{parent_id_col_idx}])) {
371 207         353 $families{$rec->[$data->{parent_id_col_idx}]}{$rec->[$data->{leaf_col_idx}]}++;
372             }
373             }
374 22         52 $error_msg = <
375             No record with a non-null value in the '$data->{parent_id_col}' column
376             may have two children with the same value in the '$data->{leaf_col}' column.
377             The following are violations:
378             ERROR_MSG_SIBLINGS_NAMED_SAME
379              
380 22         19 my $same_names = 0;
381 22         73 for my $k (sort {$a <=> $b} keys %families) {
  217         195  
382 127         81 for my $l (sort keys %{$families{$k}}) {
  127         249  
383 202 100       603 if ($families{$k}{$l} > 1) {
384 5         16 $error_msg .= " $data->{parent_id_col}: $k|$data->{leaf_col}: $l|count of $data->{leaf_col}s: $families{$k}{$l}\n";
385 5         5 $same_names++;
386             }
387             }
388             }
389 22 100       400 croak $error_msg if $same_names;
390              
391 19         31 $data->{data_records} = $data_records;
392 19         183 return $data;
393             }
394              
395             =head2 C
396              
397             =over 4
398              
399             =item * Purpose
400              
401             Identify the names of the columns in the taxonomy.
402              
403             =item * Arguments
404              
405             my $fields = $self->fields();
406              
407             No arguments; the information is already inside the object.
408              
409             =item * Return Value
410              
411             Reference to an array holding a list of the columns as they appear in the
412             header row of the incoming taxonomy file.
413              
414             =item * Comment
415              
416             Read-only.
417              
418             =back
419              
420             =head2 C
421              
422             =over 4
423              
424             =item * Purpose
425              
426             Once the taxonomy has been validated, get a list of its data rows as a Perl
427             data structure.
428              
429             =item * Arguments
430              
431             $data_records = $self->data_records;
432              
433             None.
434              
435             =item * Return Value
436              
437             Reference to array of array references. The array will hold the data records
438             found in the incoming taxonomy file in their order in that file.
439              
440             =item * Comment
441              
442             Does not contain any information about the fields in the taxonomy, so you
443             should probably either (a) use in conjunction with C method above;
444             or (b) use C.
445              
446             =back
447              
448             =cut
449              
450             =head2 C
451              
452             =over 4
453              
454             =item * Purpose
455              
456             Identify the index position of a given field within the header row.
457              
458             =item * Arguments
459              
460             $index = $self->get_field_position('income');
461              
462             Takes a single string holding the name of one of the fields (column names).
463              
464             =item * Return Value
465              
466             Integer representing the index position (counting from C<0>) of the field
467             provided as argument. Throws exception if the argument is not actually a
468             field.
469              
470             =back
471              
472             =cut
473              
474             =head2 Accessors
475              
476             The following methods provide information about key columns in a
477             Parse::Taxonomy::MaterializedPath object. The key columns are those which hold the
478             ID, parent ID and component information. They take no arguments. The methods
479             whose names end in C<_idx> return integers, as they return the index position
480             of the column in the header row. The other methods return strings.
481              
482             $index_of_id_column = $self->id_col_idx;
483              
484             $name_of_id_column = $self->id_col;
485              
486             $index_of_parent_id_column = $self->parent_id_col_idx;
487              
488             $name_of_parent_id_column = $self->parent_id_col;
489              
490             $index_of_leaf_column = $self->leaf_col_idx;
491              
492             $name_of_leaf_column = $self->leaf_col;
493              
494             =cut
495              
496             sub id_col_idx {
497 421     421 0 973 my $self = shift;
498 421         411 return $self->{id_col_idx};
499             }
500              
501             sub id_col {
502 105     105 1 83 my $self = shift;
503 105         196 return $self->{id_col};
504             }
505              
506             sub parent_id_col_idx {
507 214     214 0 123 my $self = shift;
508 214         231 return $self->{parent_id_col_idx};
509             }
510              
511             sub parent_id_col {
512 88     88 1 74 my $self = shift;
513 88         227 return $self->{parent_id_col};
514             }
515              
516             sub leaf_col_idx {
517 214     214 0 142 my $self = shift;
518 214         382 return $self->{leaf_col_idx};
519             }
520              
521             sub leaf_col {
522 71     71 1 56 my $self = shift;
523 71         158 return $self->{leaf_col};
524             }
525              
526             =head2 C
527              
528             =over 4
529              
530             =item * Purpose
531              
532             Generate a new Perl data structure which holds the same information as a
533             Parse::Taxonomy::AdjacentList object but which expresses the route from the
534             root node to a given branch or leaf node as either a separator-delimited
535             string (as in the C column of a Parse::Taxonomy::MaterializedPath object) or
536             as an array reference holding the list of names which delineate that route.
537              
538             Another way of expressing this: Transform a taxonomy-by-adjacent-list to a
539             taxonomy-by-materialized-path.
540              
541             Example: Suppose we have a CSV file which serves as a taxonomy-by-adjacent-list for
542             this data:
543              
544             "id","parent_id","name","is_actionable"
545             "1","","Alpha","0"
546             "2","","Beta","0"
547             "3","1","Epsilon","0"
548             "4","3","Kappa","1"
549             "5","1","Zeta","0"
550             "6","5","Lambda","1"
551             "7","5","Mu","0"
552             "8","2","Eta","1"
553             "9","2","Theta","1"
554              
555             Instead of having the route from the root node to a given node be represented
556             B by following Cs up the tree, suppose we want that
557             route to be represented by a string. Assuming that we work with default
558             column names, that would mean representing the information currently spread
559             out among the C, C and C columns in a single C
560             column which, by default, would hold an array reference.
561              
562             $source = "./t/data/theta.csv";
563             $self = Parse::Taxonomy::AdjacentList->new( {
564             file => $source,
565             } );
566              
567             $taxonomy_with_path_as_array = $self->pathify;
568              
569             Yielding:
570              
571             [
572             ["path", "is_actionable"],
573             [["", "Alpha"], 0],
574             [["", "Beta"], 0],
575             [["", "Alpha", "Epsilon"], 0],
576             [["", "Alpha", "Epsilon", "Kappa"], 1],
577             [["", "Alpha", "Zeta"], 0],
578             [["", "Alpha", "Zeta", "Lambda"], 1],
579             [["", "Alpha", "Zeta", "Mu"], 0],
580             [["", "Beta", "Eta"], 1],
581             [["", "Beta", "Theta"], 1],
582             ]
583              
584             If we wanted the path information represented as a string rather than an array
585             reference, we would say:
586              
587             $taxonomy_with_path_as_string = $self->pathify( { as_string => 1 } );
588              
589             Yielding:
590              
591             [
592             ["path", "is_actionable"],
593             ["|Alpha", 0],
594             ["|Beta", 0],
595             ["|Alpha|Epsilon", 0],
596             ["|Alpha|Epsilon|Kappa", 1],
597             ["|Alpha|Zeta", 0],
598             ["|Alpha|Zeta|Lambda", 1],
599             ["|Alpha|Zeta|Mu", 0],
600             ["|Beta|Eta", 1],
601             ["|Beta|Theta", 1],
602             ]
603              
604             If we are providing a true value to the C key, we also get to
605             choose what character to use as the separator in the C column.
606              
607             $taxonomy_with_path_as_string_different_path_col_sep =
608             $self->pathify( {
609             as_string => 1,
610             path_col_sep => '~~',
611             } );
612              
613             Yields:
614              
615             [
616             ["path", "is_actionable"],
617             ["~~Alpha", 0],
618             ["~~Beta", 0],
619             ["~~Alpha~~Epsilon", 0],
620             ["~~Alpha~~Epsilon~~Kappa", 1],
621             ["~~Alpha~~Zeta", 0],
622             ["~~Alpha~~Zeta~~Lambda", 1],
623             ["~~Alpha~~Zeta~~Mu", 0],
624             ["~~Beta~~Eta", 1],
625             ["~~Beta~~Theta", 1],
626             ]
627              
628             Finally, should we want the C column in the returned arrayref to be
629             named something other than I, we can provide a value to the C
630             key.
631              
632             [
633             ["foo", "is_actionable"],
634             [["", "Alpha"], 0],
635             [["", "Beta"], 0],
636             [["", "Alpha", "Epsilon"], 0],
637             [["", "Alpha", "Epsilon", "Kappa"], 1],
638             [["", "Alpha", "Zeta"], 0],
639             [["", "Alpha", "Zeta", "Lambda"], 1],
640             [["", "Alpha", "Zeta", "Mu"], 0],
641             [["", "Beta", "Eta"], 1],
642             [["", "Beta", "Theta"], 1],
643             ]
644              
645             item * Arguments
646              
647             Optional single hash reference. If provided, the following keys may be used:
648              
649             =over 4
650              
651             =item * C
652              
653             User-supplied name for column holding path information in the returned array
654             reference. Defaults to C.
655              
656             =item * C
657              
658             Boolean. If supplied with a true value, path information will be represented
659             as a separator-delimited string rather than an array reference.
660              
661             =item * C
662              
663             User-supplied string to be used to separate the parts of the route when
664             C is called with a true value. Not meaningful unless C
665             is true.
666              
667             =back
668              
669             =item * Return Value
670              
671             Reference to an array of array references. The first element in the array
672             will be a reference to an array of field names. Each succeeding element will
673             be a reference to an array holding data for one record in the original
674             taxonomy. The path data will be represented, by default, as an array
675             reference built up from the component (C) column in the original
676             taxonomy, but if C is selected, the path data in all non-header
677             elements will be a separator-delimited string.
678              
679             =back
680              
681             =cut
682              
683             sub pathify {
684 21     21 1 16840 my ($self, $args) = @_;
685 21 100       75 if (defined $args) {
686 13 100 100     80 unless (ref($args) and (reftype($args) eq 'HASH')) {
687 2         200 croak "Argument to pathify() must be hash ref";
688             }
689 11         15 my %permissible_args = map { $_ => 1 } ( qw| path_col as_string path_col_sep | );
  33         59  
690 11         10 for my $k (keys %{$args}) {
  11         29  
691             croak "'$k' is not a recognized key for pathify() argument hashref"
692 15 100       95 unless $permissible_args{$k};
693             }
694 10 100 100     34 if ($args->{path_col_sep} and not $args->{as_string}) {
695 1         69 croak "Supplying a value for key 'path_col_step' is only valid when also supplying true value for 'as_string'";
696             }
697             }
698 17 100       40 $args->{path_col} = defined($args->{path_col}) ? $args->{path_col} : 'path';
699 17 100       28 if ($args->{as_string}) {
700 6 100       15 $args->{path_col_sep} = defined($args->{path_col_sep}) ? $args->{path_col_sep} : '|';
701             }
702              
703 17         21 my @rewritten = ();
704 17         15 my @fields_in = @{$self->fields};
  17         47  
705 17         23 my @fields_out = ( $args->{path_col} );
706 17         22 for my $f (@fields_in) {
707 98 100 100     90 unless (
      100        
708             ($f eq $self->id_col) or
709             ($f eq $self->parent_id_col) or
710             ($f eq $self->leaf_col)
711             ) {
712 47         54 push @fields_out, $f;
713             }
714             }
715 17         20 push @rewritten, \@fields_out;
716              
717 17         32 my %colsin2idx = map { $fields_in[$_] => $_ } (0 .. $#fields_in);
  98         127  
718              
719 207         192 my %hashed_data = map { $_->[$self->id_col_idx] => {
720             parent_id => $_->[$self->parent_id_col_idx],
721             leaf => $_->[$self->leaf_col_idx],
722 17         23 } } @{$self->data_records};
  17         33  
723              
724 17         24 my @this_path = ();
725 17         16 my $code;
726             $code = sub {
727 466     466   285 my $id = shift;
728 466         422 push @this_path, $hashed_data{$id}{leaf};
729 466         340 my $parent_id = $hashed_data{$id}{parent_id};
730 466 100       448 if (length($parent_id)) {
731 259         152 &{$code}($parent_id);
  259         299  
732             }
733             else {
734 207         149 push @this_path, '';
735             }
736 466         304 return;
737 17         49 };
738 17         14 for my $rec (@{$self->data_records}) {
  17         27  
739 207         113 my @new_record;
740 207         221 &{$code}($rec->[$self->id_col_idx]);
  207         199  
741 207         262 my $path_as_array_ref = [ reverse @this_path ];
742 207 100       213 if ($args->{as_string}) {
743             push @new_record,
744 62         41 join($args->{path_col_sep} => @{$path_as_array_ref});
  62         87  
745             }
746             else {
747 145         125 push @new_record, $path_as_array_ref;
748             }
749 207         217 for my $f (grep { $_ ne $args->{path_col} } @fields_out) {
  822         795  
750 615         656 push @new_record, $rec->[$colsin2idx{$f}];
751             }
752 207         172 push @rewritten, \@new_record;
753 207         263 @this_path = ();
754             }
755 17         63 return \@rewritten;
756             }
757              
758             =head2 C
759              
760             =over 4
761              
762             =item * Purpose
763              
764             Create a CSV-formatted file holding the data returned by C.
765              
766             =item * Arguments
767              
768             $csv_file = $self->write_pathified_to_csv( {
769             pathified => $pathified, # output of pathify()
770             csvfile => './t/data/taxonomy_out5.csv',
771             } );
772              
773             Single hash reference. That hash is keyed on:
774              
775             =over 4
776              
777             =item * C
778              
779             B Its value must be the arrayref of hash references returned by
780             the C method.
781              
782             =item * C
783              
784             Optional. Path to location where a CSV-formatted text file holding the
785             taxonomy-by-adjacent-list will be written. Defaults to a file called
786             F in the current working directory.
787              
788             =item * Text::CSV_XS options
789              
790             You can also pass through any key-value pairs normally accepted by
791             F.
792              
793             =back
794              
795             =item * Return Value
796              
797             Returns path to CSV-formatted text file just created.
798              
799             =item * Example
800              
801             Suppose we have a CSV-formatted file holding the following taxonomy-by-adjacent-list:
802              
803             "id","parent_id","name","is_actionable"
804             "1","","Alpha","0"
805             "2","","Beta","0"
806             "3","1","Epsilon","0"
807             "4","3","Kappa","1"
808             "5","1","Zeta","0"
809             "6","5","Lambda","1"
810             "7","5","Mu","0"
811             "8","2","Eta","1"
812             "9","2","Theta","1"
813              
814             After running this file through C, C and
815             C we will have a new CSV-formatted file holding
816             this taxonomy-by-materialized-path:
817              
818             path,is_actionable
819             |Alpha,0
820             |Beta,0
821             |Alpha|Epsilon,0
822             |Alpha|Epsilon|Kappa,1
823             |Alpha|Zeta,0
824             |Alpha|Zeta|Lambda,1
825             |Alpha|Zeta|Mu,0
826             |Beta|Eta,1
827             |Beta|Theta,1
828              
829             Note that the C, C and C columns have been replaced by the column.
830              
831             =back
832              
833             =cut
834              
835             sub write_pathified_to_csv {
836 9     9 1 3739 my ($self, $args) = @_;
837 9 100       20 if (defined $args) {
838 8 100 100     184 croak "Argument to 'pathify()' must be hashref"
839             unless (ref($args) and reftype($args) eq 'HASH');
840             croak "Argument to 'pathify()' must have 'pathified' element"
841 6 100       77 unless exists $args->{pathified};
842             croak "Argument 'pathified' must be array reference"
843             unless (ref($args->{pathified}) and
844 5 100 100     159 reftype($args->{pathified}) eq 'ARRAY');
845             }
846             else {
847 1         122 croak "write_pathified_to_csv() must be supplied with hashref"
848             }
849 3         4 my $pathified = $args->{pathified};
850 3         4 delete $args->{pathified};
851              
852             # Test whether we're working with first element array ref or first element
853             # string
854 3 100       8 my $path_as_string = (! ref($pathified->[1]->[0])) ? 1 : 0;
855              
856 3         7 my $columns_in = $self->fields;
857 9         14 my %path_columns = map {$_ => 1} (
858             $self->{id_col},
859             $self->{parent_id_col},
860             $self->{leaf_col},
861 3         7 );
862             my @non_path_columns_in =
863 3         5 map { $columns_in->[$_] }
864 12         17 grep { ! $path_columns{$columns_in->[$_]} }
865 3         5 (0..$#{$columns_in});
  3         3  
866 3         5 my @columns_out = ( $pathified->[0]->[0]);
867 3         3 push @columns_out, @non_path_columns_in;
868              
869 3         5451 my $cwd = cwd();
870             my $csvfile = defined($args->{csvfile})
871             ? $args->{csvfile}
872 3 100       25 : "$cwd/taxonomy_out.csv";
873 3         7 delete $args->{csvfile};
874              
875             # By this point, we should have processed all args other than those
876             # intended for Text::CSV_XS and assigned their contents to variables as
877             # needed.
878              
879 3         13 my $csv_args = { binary => 1 };
880 3         6 while (my ($k,$v) = each %{$args}) {
  5         25  
881 2         7 $csv_args->{$k} = $v;
882             }
883 3         39 my $csv = Text::CSV_XS->new($csv_args);
884 3 50       543 open my $OUT, ">:encoding(utf8)", $csvfile
885             or croak "Unable to open $csvfile for writing";
886 3 100       7909 $csv->eol(defined($csv_args->{eol}) ? $csv_args->{eol} : "\n");
887 3         136 $csv->print($OUT, [@columns_out]);
888 3         26 for my $rec (@{$pathified}[1..$#{$pathified}]) {
  3         8  
  3         8  
889             $csv->print(
890             $OUT,
891             $path_as_string
892             ? $rec
893             : [
894 18         33 join('|' => @{$rec->[0]}),
895 27 100       103 @{$rec}[1..$#columns_out]
  18         54  
896             ]
897             );
898             }
899 3 50       136 close $OUT or croak "Unable to close $csvfile after writing";
900              
901 3         54 return $csvfile;
902             }
903              
904             1;
905              
906             # vim: formatoptions=crqot