Since you need warnings and error messages formatted the same way, and you don't need expandability, the code can be rewritten non-expandably, which allows you to use \msg_error:nn(nnnn) always (rather than \msg_expandable_error:nn(nnnn)). (the old answer, with expandable \diaa_csv_item:nnn is below, for reference.)
I changed a bit the syntax of the output of \getRow. When you do
\getRow [*] \<row macro> {<row ID>} {<CSV ID>}
the \<row macro> is defined with the syntax:
\<row macro> \<return> [<col number>]
such that \<row macro> saves the item from row <row ID>, column <col number>, from the <CSV ID> into \<return>. The previous code was expandable, so \<row macro> [<col number>] would expand directly to the return value, but now that it may throw a (non-expandable) error, the \<row macro> is protected and returns the item in \<return>, which can then be used.
All the errors and warnings you requested should be there. If the CSV database was not defined, the error csv-undefined is raised. If the CSV exists, but does not have the requested key, key-undefined is raised. Both these errors are checked in \__diaa_csv_item_exist:nnN(T). These two tests are common for \getValue and \getRow.
With \getValue, if the column requested is zero or larger than the number of columns in the CSV, error out-of-range is raised. Then, if a valid item is requested, it is checked for being empty. If it is, the warning item-empty is issued.
With \getRow, if all items in a row are empty, the row-empty warning is issued, and the \<row macro> is defined to raise the improper-row error if used. Otherwise, \<row macro> is defined such that it does the same checks specific to \getValue: checks if the column number is within range, then checks if the item returned is not empty.
Here's the code:
\begin{filecontents*}{test.csv}
Third Parameter , 7 , 9 ,
First Parameter , 5 , {foo, bar} ,
Second Parameter , 3 , 6 , 44
Empty Parameter , , ,
\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
\bool_new:N \l__diaa_empty_item_bool
\seq_new:N \l__diaa_csv_tmp_seq
\tl_new:N \l__diaa_tmpa_tl
% 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 }
{
\tl_clear:N \l__diaa_tmpa_tl
\diaa_csv_item:nnnN {#4} {#3} {#5} \l__diaa_tmpa_tl
\IfBooleanTF {#1}
{ \tl_gset_eq:NN } { \tl_set_eq:NN }
#2 \l__diaa_tmpa_tl
\tl_if_empty:NT #2
{ \msg_warning:nnnnn { diaa } { item-empty } {#3} {#4} {#5} }
}
% key, value column, label
\cs_new_protected:Npn \diaa_csv_item:nnnN #1 #2 #3 #4
{
__diaa_csv_item_exist:nnNT {#3} {#1} #4
{ \exp_args:NV __diaa_check_column_range:nn #4 {#2} }
}
\cs_new_protected:Npn __diaa_check_column_range:nn #1 #2
{
\bool_lazy_or:nnTF
{ \int_compare_p:nNn {#2} = { 0 } }
{ \int_compare_p:nNn { \tl_count:n {#1} } < { \int_abs:n {#2} } }
{ \msg_error:nnn { diaa } { out-of-range } {#2} }
{ \tl_set:Nx \l__diaa_tmpa_tl { \tl_item:nn {#1} {#2} } }
}
\prg_new_protected_conditional:Npnn __diaa_csv_item_exist:nnN #1 #2 #3 { T }
{
\prop_if_exist:cTF { g__diaa_csv_#1_prop }
{
\prop_get:cnNTF { g__diaa_csv_#1_prop } {#2} #3
{ \prg_return_true: }
{
\msg_error:nnnn { diaa } { key-undefined } {#2} {#1}
\prg_return_false:
}
}
{
\msg_error:nnn { diaa } { csv-undefined } {#1}
\prg_return_false:
}
}
\cs_new_protected:Npn __diaa_check_empty:nn #1 #2
{
\tl_if_empty:nT {#1}
{ \msg_warning:nnn { diaa } { empty-row-item } {#2} }
#1
}
% star → global assignment, macro, key, label
\NewDocumentCommand \getRow { s m m m }
{
\IfBooleanTF {#1}
{ __diaa_get_row:NNnn \cs_gset_protected:Npx }
{ __diaa_get_row:NNnn \cs_set_protected:Npx }
#2 {#3} {#4}
}
\cs_new_protected:Npn __diaa_get_row:NNnn #1 #2 #3 #4
{
#1 #2 ##1 [ ##2 ]
{ \msg_error:nnx { diaa } { improper-row } { \cs_to_str:N #2 } }
__diaa_csv_item_exist:nnNT {#4} {#3} \l__diaa_tmpa_tl
{
\bool_set_true:N \l__diaa_empty_item_bool
\tl_map_inline:Nn \l__diaa_tmpa_tl
{
\tl_if_empty:nF {##1}
{ \bool_set_false:N \l__diaa_empty_item_bool }
}
\bool_if:NT \l__diaa_empty_item_bool
{ \msg_warning:nnnn { diaa } { row-empty } {#3} {#4} }
#1 #2 ##1 [ ##2 ]
{
\exp_not:N __diaa_get_column:nnN
{ \exp_not:V \l__diaa_tmpa_tl } {##2} ##1
}
}
}
\cs_new_protected:Npn __diaa_get_column:nnN #1 #2 #3
{
\str_if_eq:nnTF {#2} { non-empty }
{
__diaa_nb_nonempty_items_in_row:nn { 0 } #1
\q_recursion_tail \q_recursion_stop
}
{ __diaa_check_column_range:nn {#1} {#2} }
}
\cs_new:Npn __diaa_nb_nonempty_items_in_row:nn #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:nn {#1} }
{ __diaa_nb_nonempty_items_in_row:nn { #1 + 1 } }
}
\msg_new:nnn { diaa } { csv-undefined } { CSV~database~#1'~undefined! } \msg_new:nnn { diaa } { key-undefined } { CSV~#2'~has~no~key~#1'! } \msg_new:nnn { diaa } { out-of-range } { Index~#1~out~of~range! } \msg_new:nnn { diaa } { item-empty } { Item~#1~from~#2'~in~CSV~#3'~is~empty! } \msg_new:nnn { diaa } { row-empty } { Row~#1'~in~CSV~`#2'~is~empty! }
\msg_new:nnn { diaa } { empty-row-item }
{ Empty~item~#1~\msg_line_context:! }
\msg_new:nnn { diaa } { improper-row }
{
Improper~row~macro~\iow_char:N \#1~\msg_line_context:.\
The~\iow_char:N \getRow~command~did~not~succeed.
}
\ExplSyntaxOff
\parindent0pt
\begin{document}
\ReadCSV{mydata}{test.csv}
\verb|\getValue\myValue{Second Parameter}{NOTmydata}|\
A \textbf{compilation-halting error} should show up stating that "NOTmydata" is undefined CSV data.\
\getValue\myValue{Second Parameter}{NOTmydata}\
\getRow\myValue{Second Parameter}{NOTmydata}; \myValue\myValuee[1]\
\verb|\getValue\myValue{Non-existent Parameter}{mydata}|\
A \textbf{compilation-halting error} should show up stating that "Non-existent Parameter" is undefined CSV parameter.\
\getValue\myValue{Non-existent Parameter}{mydata}\
\getRow\myValue{Non-existent Parameter}{mydata}; \myValue\myValuee[1]\
\verb|\getValue\myValue[4]{Second Parameter}{mydata}|\
An \textbf{error} because index 4 is out of range.\
\getValue\myValue[4]{Second Parameter}{mydata}\
\getRow\myValue{Second Parameter}{mydata}; \myValue\myValuee[4]\
\verb|\getValue\myValue{Empty Parameter}{mydata}|\
A \textbf{Warning} should show up stating that the value stored in \verb|\myValue| is empty.
\getValue\myValue{Empty Parameter}{mydata}\
\getRow\myValue{Empty Parameter}{mydata}; \myValue\myValuee[1]\
\end{document}
Previous answer, for reference:
The function \diaa_csv_item:nnn that fetches data from the stored CSV is expandable, so you can't have warnings, only expandable errors, which are themselves very limited. An expandable error will have the form:
! Undefined control sequence.
<argument> \::error
! diaa: CSV database `NOTmydata' undefined!
l.161 ...alue\myValue{Second Parameter}{NOTmydata}
that is, a low-level Undefined control sequence error from TeX (on \::error being the undefined control sequence), and one line of text following it. The message can't be too long or TeX will truncate the message.
I implemented the warning as an after-the-fact check on the return value of \getValue. Since that function is not expandable, you can have a warning. \CSVItem can't have a warning: either an expandable error or nothing. I left a version that errors with an empty item commented in the code.
Note that in the case of checking if the element of a retrieved row (that is, using \getRow\myRow... then \myRow[<index>] returning an empty value), the macro \__diaa_check_empty:nn is used. This macro is not protected, so it will expand in an expansion-only context, however it is not fully expandable because it contains a \msg_warning:nnn (and as I said before, you can't have an expandable warning), so it might break unexpectedly in expansion contexts if the retrieved item is empty. If it is not, everything works normally.
Other than that, I used \prop_if_exist:cTF to check if the CSV was defined, \prop_if_in:cnTF to check if the requested item exists in the CSV, and \tl_count:n to check if the column requested is within the range.
\begin{filecontents*}{test.csv}
Third Parameter , 7 , 9 ,
First Parameter , 5 , {foo, bar} ,
Second Parameter , 3 , 6 , 44
Empty Parameter , , ,
\end{filecontents*}
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
% Step 1: reading the file
\tl_new:N \l__diaa_tmpa_tl
\ior_new:N \l__diaa_csv_ior
\bool_new:N \l__diaa_csv_str_bool
\bool_new:N \l__diaa_empty_item_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__diaa_tmpa_tl
\ior_close:N \l__diaa_csv_ior
\seq_set_split:NnV \l_tmpa_seq { , } \l__diaa_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__diaa_tmpa_tl
\seq_map_inline:Nn \l__diaa_csv_tmp_seq
{
\tl_put_right:Nx \l__diaa_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__diaa_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} }
\tl_if_empty:NT #2
{ \msg_warning:nnnnn { diaa } { item-empty } {#3} {#4} {#5} }
}
% key, value column, label
\NewExpandableDocumentCommand \CSVItem { m O{1} m }
{ \diaa_csv_item:nnn {#1} {#2} {#3} }
% Version with error if empty
% \NewExpandableDocumentCommand \CSVItem { m O{1} m }
% {
% \exp_args:Nf __diaa_check_empty_item:nnnn
% { \diaa_csv_item:nnn {#1} {#2} {#3} } {#2} {#1} {#3}
% }
% \cs_new:Npn __diaa_check_empty_item:nnnn #1 #2 #3 #4
% {
% \tl_if_empty:nTF {#1}
% { \msg_expandable_error:nnnnn { diaa } { item-empty } {#2} {#3} {#4} }
% {#1}
% }
\cs_generate_variant:Nn \tl_item:nn { f }
% key, value column, label
\cs_new:Npn \diaa_csv_item:nnn #1 #2 #3
{
\prop_if_exist:cTF { g__diaa_csv_#3_prop }
{
\prop_if_in:cnTF { g__diaa_csv_#3_prop } {#1}
{
\exp_args:NNf __diaa_check_column_range:Nnn \use_i:nn
{ \prop_item:cn { g__diaa_csv_#3_prop } {#1} } {#2}
}
{ \msg_expandable_error:nnnn { diaa } { key-undefined } {#1} {#3} }
}
{ \msg_expandable_error:nnn { diaa } { csv-undefined } {#3} }
}
\cs_new:Npn __diaa_check_column_range:Nnn #1 #2 #3
{
\bool_lazy_or:nnTF
{ \int_compare_p:nNn {#3} = { 0 } }
{ \int_compare_p:nNn { \tl_count:n {#2} } < { \int_abs:n {#3} } }
{ \msg_expandable_error:nnn { diaa } { out-of-range } {#3} }
{ \exp_args:Nf #1 { \tl_item:nn {#2} {#3} } {#3} }
}
\msg_new:nnn { diaa } { csv-undefined } { CSV~database~#1'~undefined! } \msg_new:nnn { diaa } { key-undefined } { CSV~#2'~has~no~key~#1'! } \msg_new:nnn { diaa } { out-of-range } { Index~#1~out~of~range! } \msg_new:nnn { diaa } { item-empty } { Item~#1~from~#2'~in~CSV~#3'~is~empty! } \msg_new:nnn { diaa } { empty-row-item } { Empty~item~#1~\msg_line_context:! } \msg_new:nnn { diaa } { row-empty } { Row~#1'~in~CSV~`#2'~is~empty! }
\cs_new:Npn __diaa_check_empty:nn #1 #2
{
\tl_if_empty:nT {#1}
{ \msg_warning:nnn { diaa } { empty-row-item } {#2} }
#1
}
% star → global assignment, macro, key, label
\NewDocumentCommand \getRow { s m m m }
{
\prop_if_exist:cTF { g__diaa_csv_#4_prop }
{
\prop_get:cnNTF { g__diaa_csv_#4_prop } {#3} \l__diaa_tmpa_tl
{
\bool_set_true:N \l__diaa_empty_item_bool
\tl_map_inline:Nn \l__diaa_tmpa_tl
{
\tl_if_empty:nF {##1}
{ \bool_set_false:N \l__diaa_empty_item_bool }
}
\bool_if:NT \l__diaa_empty_item_bool
{ \msg_warning:nnnn { diaa } { row-empty } {#3} {#4} }
\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:nn { 0 }
\exp_not:V \l__diaa_tmpa_tl
\exp_not:n { \q_recursion_tail \q_recursion_stop }
}
{
\exp_not:N __diaa_check_column_range:Nnn
\exp_not:N __diaa_check_empty:nn
{ \exp_not:V \l__diaa_tmpa_tl } {##1}
}
}
}
{ \msg_error:nnnn { diaa } { key-undefined } {#3} {#4} }
}
{ \msg_error:nnn { diaa } { csv-undefined } {#4} }
}
\cs_new:Npn __diaa_nb_nonempty_items_in_row:nn #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:nn {#1} }
{ __diaa_nb_nonempty_items_in_row:nn { #1 + 1 } }
}
\ExplSyntaxOff
\parindent0pt
\begin{document}
\ReadCSV{mydata}{test.csv}
\verb|\getValue\myValue{Second Parameter}{NOTmydata}|\
A \textbf{compilation-halting error} should show up stating that "NOTmydata" is undefined CSV data.\
\getValue\myValue{Second Parameter}{NOTmydata}\
\verb|\getValue\myValue{Non-existent Parameter}{mydata}|\
A \textbf{compilation-halting error} should show up stating that "Non-existent Parameter" is undefined CSV parameter.\
\getValue\myValue{Non-existent Parameter}{mydata}\
\verb|\getValue\myValue[4]{Second Parameter}{mydata}|\
An \textbf{error} because index 4 is out of range.\
\getValue\myValue[4]{Second Parameter}{mydata}\
\verb|\getValue\myValue{Empty Parameter}{mydata}|\
A \textbf{Warning} should show up stating that the value stored in \verb|\myValue| is empty.
\getValue\myValue{Empty Parameter}{mydata}\
\getRow\myRow{Empty Parameter}{mydata}
\myRow[1]
\CSVItem{Second Parameter}[4]{mydata}
\end{document}
\diaa_csv_item:nnnis fully expandable, and you can't have expandable warnings (only errors). You can have the warning for\getValuebut not for\CSVItem. – Phelype Oleinik Dec 19 '20 at 19:48