File Coverage

blib/lib/App/CSVUtils/csv_fill_cells.pm
Criterion Covered Total %
statement 11 11 100.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 15 15 100.0


line stmt bran cond sub pod time code
1             package App::CSVUtils::csv_fill_cells;
2              
3 1     1   5195 use 5.010001;
  1         4  
4 1     1   7 use strict;
  1         3  
  1         22  
5 1     1   5 use warnings;
  1         3  
  1         69  
6              
7             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
8             our $DATE = '2023-07-25'; # DATE
9             our $DIST = 'App-CSVUtils'; # DIST
10             our $VERSION = '1.030'; # VERSION
11              
12 1         1070 use App::CSVUtils qw(
13             gen_csv_util
14             compile_eval_code
15 1     1   6 );
  1         3  
16              
17             gen_csv_util(
18             name => 'csv_fill_cells',
19             summary => 'Create a CSV and fill its cells from supplied values (a 1-column CSV)',
20             description => <<'_',
21              
22             This utility takes values (from cells of a 1-column input CSV), creates an
23             output CSV of specified size, and fills the output CSV in one of several
24             possible ways ("layouts"): left-to-right first then top-to-bottom, or
25             bottom-to-top then left-to-right, etc.
26              
27             Some illustration of the layout:
28              
29             % cat 1-to-100.csv
30             num
31             1
32             2
33             3
34             ...
35             100
36              
37             % csv-fill-cells 1-to-100.csv --num-rows 10 --num-fields 10 ; # default layout is 'left_to_right_then_top_to_bottom'
38             field0,field1,field2,field3,field4,field5,field6,field7,field8,field9
39             1,2,3,4,5,6,7,8,9,10
40             11,12,13,14,15,16,17,18,19,20
41             21,22,23,24,25,26,27,28,29,30
42             ...
43             91,92,93,94,95,96,97,98,99,100
44              
45             % csv-fill-cells 1-to-100.csv --num-rows 10 --num-fields 10 --layout top_to_bottom_then_left_to_right
46             field0,field1,field2,field3,field4,field5,field6,field7,field8,field9
47             1,11,21,31,41,51,61,71,81,91
48             2,12,22,32,42,52,62,72,82,92
49             3,13,23,33,43,53,63,73,83,93
50             ...
51             10,20,30,40,50,60,70,80,90,100
52              
53             % csv-fill-cells 1-to-100.csv --num-rows 10 --num-fields 10 --layout top_to_bottom_then_right_to_left
54             91,81,71,61,51,41,31,21,11,1
55             92,82,72,62,52,42,32,22,12,2
56             93,83,73,63,53,43,33,23,13,3
57             ...
58             100,90,80,70,60,50,40,30,20,10
59              
60             % csv-fill-cells 1-to-100.csv --num-rows 10 --num-fields 10 --layout right_to_left_then_top_to_bottom
61             10,9,8,7,6,5,4,3,2,1
62             20,19,18,17,16,15,14,13,12,11
63             30,29,28,27,26,25,24,23,22,21
64             ...
65             100,99,98,97,96,95,94,93,92,91
66              
67             Some additional options are available, e.g.: a filter to let skip filling some
68             cells.
69              
70             When there are more input values than can be fitted, the extra input values are
71             not placed into the output CSV.
72              
73             When there are less input values to fill the specified number of rows, then only
74             the required number of rows and/or columns will be used.
75              
76             Additional options planned:
77              
78             - what to do when there are less values to completely fill the output CSV
79             (whether to always expand or expand when necessary, which is the default).
80              
81             - what to do when there are more values (extend the table or ignore the extra
82             input values, which is the default).
83              
84             _
85             add_args => {
86             # TODO
87             #fields => $App::CSVUtils::argspecopt_fields{fields}, # category:output
88              
89             layout => {
90             summary => 'Specify how the output CSV is to be filled',
91             schema => ['str*', in=>[
92             'left_to_right_then_top_to_bottom',
93             'right_to_left_then_top_to_bottom',
94             'left_to_right_then_bottom_to_top',
95             'right_to_left_then_bottom_to_top',
96             'top_to_bottom_then_left_to_right',
97             'top_to_bottom_then_right_to_left',
98             'bottom_to_top_then_left_to_right',
99             'bottom_to_top_then_right_to_left',
100             ]],
101             default => 'left_to_right_then_top_to_bottom',
102             tags => ['category:layout'],
103             },
104              
105             filter => {
106             summary => 'Code to filter cells to fill',
107             schema => 'str*',
108             description => <<'_',
109              
110             Code will be compiled in the `main` package.
111              
112             Code is passed `($r, $output_row_num, $output_field_idx)` where `$r` is the
113             stash, `$output_row_num` is a 1-based integer (first data row means 1), and
114             `$output_field_idx` is the 0-based field index (0 means the first index). Code
115             is expected to return a boolean value, where true meaning the cell should be
116             filied.
117              
118             _
119             tags => ['category:filtering'],
120             },
121             num_rows => {
122             summary => 'Number of rows of the output CSV',
123             schema => 'posint*',
124             req => 1,
125             tags => ['category:output'],
126             },
127             num_fields => {
128             summary => 'Number of fields of the output CSV',
129             schema => 'posint*',
130             req => 1,
131             tags => ['category:output'],
132             },
133             },
134              
135             tags => ['category:generating', 'accepts-code'],
136              
137             examples => [
138             {
139             summary => 'Fill number 1..100 into a 10x10 grid',
140             src => q{seq 1 100 | [[prog]] --num-rows 10 --num-fields 10},
141             src_plang => 'bash',
142             test => 0,
143             },
144             ],
145              
146             on_input_header_row => sub {
147             my $r = shift;
148              
149             # set output fields
150             $r->{output_fields} = [ map {"field$_"}
151             0 .. $r->{util_args}{num_fields}-1 ];
152              
153             # compile filter
154             if ($r->{util_args}{filter}) {
155             my $code = compile_eval_code($r->{util_args}{filter}, 'filter');
156             # this is a key we add to the stash
157             $r->{filter} = $code;
158             }
159              
160             # this is a key we add to the stash
161             $r->{input_values} = [];
162             },
163              
164             on_input_data_row => sub {
165             my $r = shift;
166              
167             push @{ $r->{input_values} }, $r->{input_row}[0];
168             },
169              
170             after_read_input => sub {
171             my $r = shift;
172              
173             my $i = -1;
174             my $layout = $r->{util_args}{layout} // 'left_to_right_then_top_to_bottom';
175             my $output_rows = [];
176              
177             my $x = $layout =~ /left_to_right/ ? 0 : $r->{util_args}{num_fields}-1;
178             my $y = $layout =~ /top_to_bottom/ ? 1 : $r->{util_args}{num_rows};
179             while (1) {
180             goto INC_POS if $r->{filter} && !$r->{filter}->($r, $y, $x);
181              
182             INC_I:
183             $i++;
184             last if $i >= @{ $r->{input_values} };
185              
186             FILL_CELL:
187             for (1 .. $y) {
188             $output_rows->[$_-1] //= [map {undef} 1 .. $r->{util_args}{num_fields}];
189             }
190             $output_rows->[$y-1][$x] = $r->{input_values}[$i];
191              
192             INC_POS:
193             if ($layout =~ /\A(top|bottom)_/) {
194             # vertically first then horizontally
195             if ($layout =~ /top_to_bottom/) {
196             $y++;
197             if ($y > $r->{util_args}{num_rows}) {
198             $y = 1;
199             if ($layout =~ /left_to_right/) {
200             $x++;
201             last if $x >= $r->{util_args}{num_fields};
202             } else {
203             $x--;
204             last if $x < 0;
205             }
206             }
207             } else {
208             $y--;
209             if ($y < 1) {
210             $y = $r->{util_args}{num_rows};
211             if ($layout =~ /left_to_right/) {
212             $x++;
213             last if $x >= $r->{util_args}{num_fields};
214             } else {
215             $x--;
216             last if $x < 0;
217             }
218             }
219             }
220             } else {
221             # horizontally first then vertically
222             if ($layout =~ /left_to_right/) {
223             $x++;
224             if ($x >= $r->{util_args}{num_fields}) {
225             $x = 0;
226             if ($layout =~ /top_to_bottom/) {
227             $y++;
228             last if $y > $r->{util_args}{num_rows};
229             } else {
230             $y--;
231             last if $y < 1;
232             }
233             }
234             } else {
235             $x--;
236             if ($x < 0) {
237             $x = $r->{util_args}{num_fields}-1;
238             if ($layout =~ /top_to_bottom/) {
239             $y++;
240             last if $y > $r->{util_args}{num_rows};
241             } else {
242             $y--;
243             last if $y < 1;
244             }
245             }
246             }
247             }
248             }
249              
250             # print rows
251             for my $row (@$output_rows) {
252             $r->{code_print_row}->($row);
253             }
254             },
255             );
256              
257             1;
258             # ABSTRACT: Create a CSV and fill its cells from supplied values (a 1-column CSV)
259              
260             __END__
261              
262             =pod
263              
264             =encoding UTF-8
265              
266             =head1 NAME
267              
268             App::CSVUtils::csv_fill_cells - Create a CSV and fill its cells from supplied values (a 1-column CSV)
269              
270             =head1 VERSION
271              
272             This document describes version 1.030 of App::CSVUtils::csv_fill_cells (from Perl distribution App-CSVUtils), released on 2023-07-25.
273              
274             =head1 FUNCTIONS
275              
276              
277             =head2 csv_fill_cells
278              
279             Usage:
280              
281             csv_fill_cells(%args) -> [$status_code, $reason, $payload, \%result_meta]
282              
283             Create a CSV and fill its cells from supplied values (a 1-column CSV).
284              
285             This utility takes values (from cells of a 1-column input CSV), creates an
286             output CSV of specified size, and fills the output CSV in one of several
287             possible ways ("layouts"): left-to-right first then top-to-bottom, or
288             bottom-to-top then left-to-right, etc.
289              
290             Some illustration of the layout:
291              
292             % cat 1-to-100.csv
293             num
294             1
295             2
296             3
297             ...
298             100
299            
300             % csv-fill-cells 1-to-100.csv --num-rows 10 --num-fields 10 ; # default layout is 'left_to_right_then_top_to_bottom'
301             field0,field1,field2,field3,field4,field5,field6,field7,field8,field9
302             1,2,3,4,5,6,7,8,9,10
303             11,12,13,14,15,16,17,18,19,20
304             21,22,23,24,25,26,27,28,29,30
305             ...
306             91,92,93,94,95,96,97,98,99,100
307            
308             % csv-fill-cells 1-to-100.csv --num-rows 10 --num-fields 10 --layout top_to_bottom_then_left_to_right
309             field0,field1,field2,field3,field4,field5,field6,field7,field8,field9
310             1,11,21,31,41,51,61,71,81,91
311             2,12,22,32,42,52,62,72,82,92
312             3,13,23,33,43,53,63,73,83,93
313             ...
314             10,20,30,40,50,60,70,80,90,100
315            
316             % csv-fill-cells 1-to-100.csv --num-rows 10 --num-fields 10 --layout top_to_bottom_then_right_to_left
317             91,81,71,61,51,41,31,21,11,1
318             92,82,72,62,52,42,32,22,12,2
319             93,83,73,63,53,43,33,23,13,3
320             ...
321             100,90,80,70,60,50,40,30,20,10
322            
323             % csv-fill-cells 1-to-100.csv --num-rows 10 --num-fields 10 --layout right_to_left_then_top_to_bottom
324             10,9,8,7,6,5,4,3,2,1
325             20,19,18,17,16,15,14,13,12,11
326             30,29,28,27,26,25,24,23,22,21
327             ...
328             100,99,98,97,96,95,94,93,92,91
329              
330             Some additional options are available, e.g.: a filter to let skip filling some
331             cells.
332              
333             When there are more input values than can be fitted, the extra input values are
334             not placed into the output CSV.
335              
336             When there are less input values to fill the specified number of rows, then only
337             the required number of rows and/or columns will be used.
338              
339             Additional options planned:
340              
341             =over
342              
343             =item * what to do when there are less values to completely fill the output CSV
344             (whether to always expand or expand when necessary, which is the default).
345              
346             =item * what to do when there are more values (extend the table or ignore the extra
347             input values, which is the default).
348              
349             =back
350              
351             This function is not exported.
352              
353             Arguments ('*' denotes required arguments):
354              
355             =over 4
356              
357             =item * B<filter> => I<str>
358              
359             Code to filter cells to fill.
360              
361             Code will be compiled in the C<main> package.
362              
363             Code is passed C<($r, $output_row_num, $output_field_idx)> where C<$r> is the
364             stash, C<$output_row_num> is a 1-based integer (first data row means 1), and
365             C<$output_field_idx> is the 0-based field index (0 means the first index). Code
366             is expected to return a boolean value, where true meaning the cell should be
367             filied.
368              
369             =item * B<inplace> => I<true>
370              
371             Output to the same file as input.
372              
373             Normally, you output to a different file than input. If you try to output to the
374             same file (C<-o INPUT.csv -O>) you will clobber the input file; thus the utility
375             prevents you from doing it. However, with this C<--inplace> option, you can
376             output to the same file. Like perl's C<-i> option, this will first output to a
377             temporary file in the same directory as the input file then rename to the final
378             file at the end. You cannot specify output file (C<-o>) when using this option,
379             but you can specify backup extension with C<-b> option.
380              
381             Some caveats:
382              
383             =over
384              
385             =item * if input file is a symbolic link, it will be replaced with a regular file;
386              
387             =item * renaming (implemented using C<rename()>) can fail if input filename is too long;
388              
389             =item * value specified in C<-b> is currently not checked for acceptable characters;
390              
391             =item * things can also fail if permissions are restrictive;
392              
393             =back
394              
395             =item * B<inplace_backup_ext> => I<str> (default: "")
396              
397             Extension to add for backup of input file.
398              
399             In inplace mode (C<--inplace>), if this option is set to a non-empty string, will
400             rename the input file using this extension as a backup. The old existing backup
401             will be overwritten, if any.
402              
403             =item * B<input_escape_char> => I<str>
404              
405             Specify character to escape value in field in input CSV, will be passed to Text::CSV_XS.
406              
407             Defaults to C<\\> (backslash). Overrides C<--input-tsv> option.
408              
409             =item * B<input_filename> => I<filename> (default: "-")
410              
411             Input CSV file.
412              
413             Use C<-> to read from stdin.
414              
415             Encoding of input file is assumed to be UTF-8.
416              
417             =item * B<input_header> => I<bool> (default: 1)
418              
419             Specify whether input CSV has a header row.
420              
421             By default, the first row of the input CSV will be assumed to contain field
422             names (and the second row contains the first data row). When you declare that
423             input CSV does not have header row (C<--no-input-header>), the first row of the
424             CSV is assumed to contain the first data row. Fields will be named C<field1>,
425             C<field2>, and so on.
426              
427             =item * B<input_quote_char> => I<str>
428              
429             Specify field quote character in input CSV, will be passed to Text::CSV_XS.
430              
431             Defaults to C<"> (double quote). Overrides C<--input-tsv> option.
432              
433             =item * B<input_sep_char> => I<str>
434              
435             Specify field separator character in input CSV, will be passed to Text::CSV_XS.
436              
437             Defaults to C<,> (comma). Overrides C<--input-tsv> option.
438              
439             =item * B<input_tsv> => I<true>
440              
441             Inform that input file is in TSV (tab-separated) format instead of CSV.
442              
443             Overriden by C<--input-sep-char>, C<--input-quote-char>, C<--input-escape-char>
444             options. If one of those options is specified, then C<--input-tsv> will be
445             ignored.
446              
447             =item * B<layout> => I<str> (default: "left_to_right_then_top_to_bottom")
448              
449             Specify how the output CSV is to be filled.
450              
451             =item * B<num_fields>* => I<posint>
452              
453             Number of fields of the output CSV.
454              
455             =item * B<num_rows>* => I<posint>
456              
457             Number of rows of the output CSV.
458              
459             =item * B<output_always_quote> => I<bool> (default: 0)
460              
461             Whether to always quote values.
462              
463             When set to false (the default), values are quoted only when necessary:
464              
465             field1,field2,"field three contains comma (,)",field4
466              
467             When set to true, then all values will be quoted:
468              
469             "field1","field2","field three contains comma (,)","field4"
470              
471             =item * B<output_escape_char> => I<str>
472              
473             Specify character to escape value in field in output CSV, will be passed to Text::CSV_XS.
474              
475             This is like C<--input-escape-char> option but for output instead of input.
476              
477             Defaults to C<\\> (backslash). Overrides C<--output-tsv> option.
478              
479             =item * B<output_filename> => I<filename>
480              
481             Output filename.
482              
483             Use C<-> to output to stdout (the default if you don't specify this option).
484              
485             Encoding of output file is assumed to be UTF-8.
486              
487             =item * B<output_header> => I<bool>
488              
489             Whether output CSV should have a header row.
490              
491             By default, a header row will be output I<if> input CSV has header row. Under
492             C<--output-header>, a header row will be output even if input CSV does not have
493             header row (value will be something like "col0,col1,..."). Under
494             C<--no-output-header>, header row will I<not> be printed even if input CSV has
495             header row. So this option can be used to unconditionally add or remove header
496             row.
497              
498             =item * B<output_quote_char> => I<str>
499              
500             Specify field quote character in output CSV, will be passed to Text::CSV_XS.
501              
502             This is like C<--input-quote-char> option but for output instead of input.
503              
504             Defaults to C<"> (double quote). Overrides C<--output-tsv> option.
505              
506             =item * B<output_quote_empty> => I<bool> (default: 0)
507              
508             Whether to quote empty values.
509              
510             When set to false (the default), empty values are not quoted:
511              
512             field1,field2,,field4
513              
514             When set to true, then empty values will be quoted:
515              
516             field1,field2,"",field4
517              
518             =item * B<output_sep_char> => I<str>
519              
520             Specify field separator character in output CSV, will be passed to Text::CSV_XS.
521              
522             This is like C<--input-sep-char> option but for output instead of input.
523              
524             Defaults to C<,> (comma). Overrides C<--output-tsv> option.
525              
526             =item * B<output_tsv> => I<bool>
527              
528             Inform that output file is TSV (tab-separated) format instead of CSV.
529              
530             This is like C<--input-tsv> option but for output instead of input.
531              
532             Overriden by C<--output-sep-char>, C<--output-quote-char>, C<--output-escape-char>
533             options. If one of those options is specified, then C<--output-tsv> will be
534             ignored.
535              
536             =item * B<overwrite> => I<bool>
537              
538             Whether to override existing output file.
539              
540              
541             =back
542              
543             Returns an enveloped result (an array).
544              
545             First element ($status_code) is an integer containing HTTP-like status code
546             (200 means OK, 4xx caller error, 5xx function error). Second element
547             ($reason) is a string containing error message, or something like "OK" if status is
548             200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
549             element (%result_meta) is called result metadata and is optional, a hash
550             that contains extra information, much like how HTTP response headers provide additional metadata.
551              
552             Return value: (any)
553              
554             =head1 HOMEPAGE
555              
556             Please visit the project's homepage at L<https://metacpan.org/release/App-CSVUtils>.
557              
558             =head1 SOURCE
559              
560             Source repository is at L<https://github.com/perlancar/perl-App-CSVUtils>.
561              
562             =head1 AUTHOR
563              
564             perlancar <perlancar@cpan.org>
565              
566             =head1 CONTRIBUTING
567              
568              
569             To contribute, you can send patches by email/via RT, or send pull requests on
570             GitHub.
571              
572             Most of the time, you don't need to build the distribution yourself. You can
573             simply modify the code, then test via:
574              
575             % prove -l
576              
577             If you want to build the distribution (e.g. to try to install it locally on your
578             system), you can install L<Dist::Zilla>,
579             L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
580             L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
581             Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
582             that are considered a bug and can be reported to me.
583              
584             =head1 COPYRIGHT AND LICENSE
585              
586             This software is copyright (c) 2023, 2022, 2021, 2020, 2019, 2018, 2017, 2016 by perlancar <perlancar@cpan.org>.
587              
588             This is free software; you can redistribute it and/or modify it under
589             the same terms as the Perl 5 programming language system itself.
590              
591             =head1 BUGS
592              
593             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=App-CSVUtils>
594              
595             When submitting a bug or request, please include a test-file or a
596             patch to an existing test-file that illustrates the bug or desired
597             feature.
598              
599             =cut