I've added a \getRow
command to the “Second method” of the existing code, as you asked. Its syntax is:
\getRow[*] {\macro} {key} {label}
The star form performs a global assignment to \macro (otherwise, the assignment is local).
The label must identify a file you've previously read using \ReadCSV
(it corresponds to the first mandatory argument of \ReadCSV
).
The key is used to select a particular row from that file (it has same meaning as with \getValue
and \CSVItem
).
After such a \getRow
call, \macro is safe to use in expansion-only contexts (e.g., inside \edef
, \write
, \num
from siunitx
, etc.): it wraps the desired value using the \unexpanded
e-TeX primitive, just like \tl_item:nn
does.
Negative indices are supported and count from the end:
As a special case, \macro[non-empty]
expands to the number of non-empty items in the row stored inside \macro.
\begin{filecontents*}{test.csv}
Third Parameter , 7 , 9 ,
First Parameter , 5 , {foo, bar} ,
Second Parameter , 3 , 6 , 44
\end{filecontents*}
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
% Step 1: reading the file
\ior_new:N \l__diaa_csv_ior
\bool_new:N \l__diaa_csv_str_bool
\seq_new:N \l__diaa_csv_tmp_seq
% str mode (bool/star), key column, label, value columns, file
\NewDocumentCommand \ReadCSV { s O{1} m O{} m }
{
\IfBooleanTF {#1}
{ \bool_set_true:N \l__diaa_csv_str_bool }
{ \bool_set_false:N \l__diaa_csv_str_bool }
\diaa_csv_read:nnnn {#3} {#2} {#4} {#5}
}
% label, key column, value columns, file
\cs_new_protected:Npn \diaa_csv_read:nnnn #1 #2 #3 #4
{
\tl_if_blank:nTF {#3} % Detect number of columns and use 2 to last
{
\ior_open:NnTF \l__diaa_csv_ior {#4}
{
\bool_if:NTF \l__diaa_csv_str_bool
{ \ior_str_get:NN }
{ \ior_get:NN }
\l__diaa_csv_ior \l_tmpa_tl
\ior_close:N \l__diaa_csv_ior
\seq_set_split:NnV \l_tmpa_seq { , } \l_tmpa_tl
\seq_clear:N \l__diaa_csv_tmp_seq
\int_step_inline:nnn { 2 } { \seq_count:N \l_tmpa_seq }
{ \seq_put_right:Nn \l__diaa_csv_tmp_seq {##1} }
}
{ \msg_error:nnn { diaa } { file-not-found } {#4} }
}
{ \seq_set_split:Nnn \l__diaa_csv_tmp_seq { , } {#3} } % explicit columns
\ior_open:NnTF \l__diaa_csv_ior {#4}
{
\prop_new:c { g__diaa_csv_#1_prop }
\__diaa_csv_read:nn {#1} {#2}
\ior_close:N \l__diaa_csv_ior
}
{ \msg_error:nnn { diaa } { file-not-found } {#4} }
}
\msg_new:nnn { diaa } { file-not-found }
{ File~`#1'~not~found. }
\cs_generate_variant:Nn \prop_put:Nnn { cxV }
% label, key column
\cs_new_protected:Npn \__diaa_csv_read:nn #1 #2
{
\bool_if:NTF \l__diaa_csv_str_bool
{ \ior_str_map_inline:Nn }
{ \ior_map_inline:Nn }
\l__diaa_csv_ior
{
\seq_set_split:Nnn \l_tmpa_seq { , } {##1} % split one CSV row
\tl_clear:N \l_tmpa_tl
\seq_map_inline:Nn \l__diaa_csv_tmp_seq
{
\tl_put_right:Nx \l_tmpa_tl { { \seq_item:Nn \l_tmpa_seq {####1} } }
}
\prop_put:cxV { g__diaa_csv_#1_prop }
{ \seq_item:Nn \l_tmpa_seq {#2} }
\l_tmpa_tl
}
}
% Step 2: getting the values
% star → global assignment, macro or tl var, value column, key, label
\NewDocumentCommand \getValue { s m O{1} m m }
{
\IfBooleanTF {#1} { \tl_gset:Nx } { \tl_set:Nx }
#2 { \diaa_csv_item:nnn {#4} {#3} {#5} }
}
% key, value column, label
\NewExpandableDocumentCommand \CSVItem { m O{1} m }
{ \diaa_csv_item:nnn {#1} {#2} {#3} }
\cs_generate_variant:Nn \tl_item:nn { f }
% key, value column, label
\cs_new:Npn \diaa_csv_item:nnn #1 #2 #3
{
\tl_item:fn { \prop_item:cn { g__diaa_csv_#3_prop } {#1} } {#2}
}
% star → global assignment, macro, key, label
\NewDocumentCommand \getRow { s m m m }
{
\prop_get:cnN { g__diaa_csv_#4_prop } {#3} \l_tmpa_tl
\IfBooleanTF {#1} { \cs_gset_nopar:Npx } { \cs_set_nopar:Npx } #2 [ ##1 ]
{
\exp_not:N \str_if_eq:nnTF {##1} { non-empty }
{
\exp_not:N \__diaa_nb_nonempty_items_in_row:nw { 0 }
\exp_not:V \l_tmpa_tl
\exp_not:n { \q_recursion_tail \q_recursion_stop }
}
{ \exp_not:N \tl_item:nn { \exp_not:V \l_tmpa_tl } {##1} }
}
}
\cs_new:Npn \__diaa_nb_nonempty_items_in_row:nw #1#2
{
\quark_if_recursion_tail_stop_do:nn {#2} { \int_eval:n {#1} }
\tl_if_empty:nTF {#2}
{ \__diaa_nb_nonempty_items_in_row:nw {#1} }
{ \__diaa_nb_nonempty_items_in_row:nw { #1 + 1 } }
}
\ExplSyntaxOff
\begin{document}
% Use default column for the key (1). The second empty optional argument (list
% of value columns) means we want to autodetect the value columns; then, the
% first column is for keys and all other columns are used as value columns.
\ReadCSV{mydata}{test.csv}
\getValue\rdPar{Second Parameter}{mydata}
\rdPar % 3
\getValue\rdPar[2]{Second Parameter}{mydata}
\rdPar % 6
\getValue\rdPar[3]{Second Parameter}{mydata}
\rdPar % 44
\getValue\rdPar{Third Parameter}{mydata}
\rdPar % 7
\edef\rdPar{\CSVItem{First Parameter}{mydata}}%
\rdPar % 5
\edef\rdPar{\CSVItem{First Parameter}[2]{mydata}}%
\rdPar % foo, bar
\edef\rdPar{\CSVItem{First Parameter}[3]{mydata}}%
\ifx\rdPar\empty
\textlangle empty\textrangle
\else
\rdPar
\fi
\getRow{\RowA}{First Parameter}{mydata}
\getRow{\RowB}{Second Parameter}{mydata}
\getRow{\RowC}{Third Parameter}{mydata}
``\RowA[1]'', ``\RowA[2]'', ``\RowA[3]'' ($\RowA[non-empty]$ non-empty)\par
``\RowB[1]'', ``\RowB[2]'', ``\RowB[3]'' ($\RowB[non-empty]$ non-empty)\par
``\RowC[1]'', ``\RowC[2]'', ``\RowC[3]'' ($\RowC[non-empty]$ non-empty)
{% Enter a group and perform a global assignment
\getRow*{\globallyDefined}{First Parameter}{mydata}%
}% Leave the group
``\globallyDefined[1]'', ``\globallyDefined[2]'', ``\globallyDefined[3]''
\edef\zzz{abc'\RowA[2]'def}
%\show\zzz % prints \zzz=macro:->abc'foo, bar'def.
\edef\zzz{\RowA[non-empty]}
%\show\zzz % prints \zzz=macro:->2.
\end{document}
datatool
(not sure)...