% \iffalse meta-comment
%
%% File l3dt.dtx Copyright (C) 2011-2013 The LaTeX3 Project
%%
%% It may be distributed and/or modified under the conditions of the
%% LaTeX Project Public License (LPPL), either version 1.3c of this
%% license or (at your option) any later version.  The latest version
%% of this license is in the file
%%
%%    http://www.latex-project.org/lppl.txt
%%
%% This file is part of the "l3experimental bundle" (The Work in LPPL)
%% and all files in that bundle must be distributed together.
%%
%% The released version of this bundle is available from CTAN.
%%
%% -----------------------------------------------------------------------
%%
%% The development version of the bundle can be found at
%%
%%    http://www.latex-project.org/svnroot/experimental/trunk/
%%
%% for those people who are interested.
%%
%%%%%%%%%%%
%% NOTE: %%
%%%%%%%%%%%
%%
%%   Snapshots taken from the repository represent work in progress and may
%%   not work or may contain conflicting material!  We therefore ask
%%   people _not_ to put them into distributions, archives, etc. without
%%   prior consultation with the LaTeX Project Team.
%%
%% -----------------------------------------------------------------------
%%
%
%<*driver|package>
\RequirePackage{expl3}
\GetIdInfo$Id: l3dt.dtx 4482 2013-04-24 21:05:12Z joseph $
  {L3 Experimental data tables}
%</driver|package>
%<*driver>
\documentclass[full]{l3doc}
\begin{document}
  \DocInput{\jobname.dtx}
\end{document}
%</driver>
% \fi
%
% \title{^^A
%   The \pkg{l3dt} package\\ Data tables^^A
%   \thanks{This file describes v\ExplFileVersion,
%      last revised \ExplFileDate.}^^A
% }
%
% \author{^^A
%  The \LaTeX3 Project\thanks
%    {^^A
%      E-mail:
%        \href{mailto:latex-team@latex-project.org}
%          {latex-team@latex-project.org}^^A
%    }^^A
% }
%
% \date{Released \ExplFileDate}
%
% \maketitle
%
% \begin{documentation}
%
% \LaTeX3 implements a \enquote{data table} variable type, which is made up of
% a series of rows each of which contain a number of key--value pairs. Thus
% a data table is in effect an array of property lists. The rows of the table
% are stored in a fixed order, and are numbered consecutively from one.
% In the same way, the order of keys (columns) is recorded in a sequence-like
% manner, again indexed from one.
%
% Within each row in a data table each entry must have a unique \meta{key}: if
% an entry is added to a row within a data table which already contains the
% \meta{key} then the new entry will overwrite the existing one. The
% \meta{keys} are compared on a string basis, using the same method as
% \cs{str_if_eq:nn}.
%
% \section{Creating and initialising data tables}
%
% \begin{function}{\dt_new:N}
%   \begin{syntax}
%     \cs{dt_new:N} \meta{data table}
%   \end{syntax}
%   Creates a new \meta{data table} or raises an error if the name is
%   already taken. The declaration is global. The \meta{property lists} will
%   initially contain no entries.
% \end{function}
%
% \begin{function}{\dt_clear:N, \dt_gclear:N}
%   \begin{syntax}
%     \cs{dt_clear:N} \meta{data table}
%   \end{syntax}
%   Clears all entries and keys from the \meta{data table}.
% \end{function}
%
% \begin{function}{\dt_clear_new:N, \dt_gclear_new:N}
%   \begin{syntax}
%     \cs{dt_clear_new:N} \meta{data table}
%   \end{syntax}
%   Ensures that the \meta{data table} exists globally by applying
%   \cs{dt_new:N} if necessary, then applies \cs{dt_(g)clear:N} to leave
%   the table empty.
% \end{function}
%
% \begin{function}{\dt_set_eq:NN, \dt_gset_eq:NN}
%   \begin{syntax}
%     \cs{dt_set_eq:NN} \meta{data table1} \meta{data table2}
%   \end{syntax}
%   Sets the content of \meta{data table1} equal to that of
%   \meta{data table2}.
% \end{function}
%
% \section{Adding data}
%
% \begin{function}{\dt_add_key:Nn, \dt_gadd_key:Nn}
%   \begin{syntax}
%     \cs{dt_add_key:Nn} \meta{dt} \Arg{key}
%   \end{syntax}
%   Adds the \meta{key} to the list of those in the \meta{data table}. The
%   \meta{key} will be converted to a string using \cs{tl_to_str:n}, and thus
%   category codes in the \meta{key} are ignored. If the \meta{key} is already
%   present in the \meta{data table} then no action is taken.
% \end{function}
%
% \begin{function}{\dt_add_row:N, \dt_gadd_row:N}
%   \begin{syntax}
%     \cs{dt_add_row:N} \meta{dt}
%   \end{syntax}
%   Adds a new row to the \meta{data table}. This will initially contain
%   no entries: all keys will be blank.
% \end{function}
%
% \begin{function}{\dt_put:Nnn, \dt_gput:Nnn}
%   \begin{syntax}
%     \cs{dt_put:Nnn} \meta{dt} \Arg{key} \Arg{value}
%   \end{syntax}
%   Adds an entry to the current row of the \meta{data table} which may be
%   accessed using the \meta{key} and which has \meta{value}. Both the
%   \meta{key} and \meta{value} may contain any \meta{balanced text}. The
%   \meta{key} is stored after processing with \cs{tl_to_str:n}, meaning
%   that category codes are ignored. If the \meta{key} is already present
%   in the current row of the \meta{data table}, the existing entry is
%   overwritten by the new \meta{value}.
% \end{function}
%
% \begin{function}{\dt_put:Nnnn, \dt_gput:Nnnn}
%   \begin{syntax}
%     \cs{dt_put:Nnnn} \meta{dt} \Arg{row} \Arg{key} \Arg{value}
%   \end{syntax}
%   Adds an entry to the \meta{row} of the \meta{data table} which may be
%   accessed using the \meta{key} and which has \meta{value}. Both the
%   \meta{key} and \meta{value} may contain any \meta{balanced text}. The
%   \meta{key} is stored after processing with \cs{tl_to_str:n}, meaning
%   that category codes are ignored. If the \meta{key} is already present
%   in the \meta{row} of the \meta{data table}, the existing entry is
%   overwritten by the new \meta{value}. The \meta{row} should be given as
%   an \meta{integer expression}.
% \end{function}
%
% \section{Removing data}
%
% \begin{function}{\dt_remove:Nn, \dt_gremove:Nn}
%   \begin{syntax}
%     \cs{dt_remove:Nn} \meta{dt} \Arg{key}
%   \end{syntax}
%   Deletes any entry from the current row of the \meta{data table} with
%   the \meta{key}. The \meta{key} is compared after processing with
%   \cs{tl_to_str:n}, meaning that category codes are ignored. Deleting of
%   all entries from a row does not delete the row itself.
% \end{function}
%
% \begin{function}{\dt_remove:Nnn, \dt_gremove:Nnn}
%   \begin{syntax}
%     \cs{dt_remove:Nnn} \meta{dt} \Arg{row} \Arg{key}
%   \end{syntax}
%   Deletes any entry from the \meta{row} of the \meta{data table} with
%   the \meta{key}. The \meta{key} is compared after processing with
%   \cs{tl_to_str:n}, meaning that category codes are ignored. The \meta{row}
%   may be given as an \meta{integer expression}. Deleting of
%   all entries from a row does not delete the row itself.
% \end{function}
%
% \begin{function}{\dt_remove_key:Nn, \dt_gremove_key:Nn}
%   \begin{syntax}
%     \cs{dt_remove_key:N} \meta{data table} \Arg{key}
%   \end{syntax}
%   Removes the \meta{key} from the \meta{data table} if it is present.
%   The \meta{key} and any associated \meta{value} will be removed from any
%   row that it is found in.
% \end{function}
%
% \begin{function}{\dt_remove_row:Nn, \dt_gremove_row:Nn}
%   \begin{syntax}
%     \cs{dt_remove_row:Nn} \meta{data table} \Arg{row}
%   \end{syntax}
%   Removes the \meta{row} (given as an \meta{integer expressions}) from the
%   \meta{data table}. The remaining rows of the table will be renumbered
%   such that they are sequential.
% \end{function}
%
% \section{Recovering information}
%
% \begin{function}[EXP]{\dt_keys:N}
%   \begin{syntax}
%     \cs{dt_keys:N} \meta{dt}
%   \end{syntax}
%   Leaves the number of keys in the \meta{data table} in the input
%   stream as an \meta{integer denotation}.
% \end{function}
%
% \begin{function}[EXP]{\dt_rows:N}
%   \begin{syntax}
%     \cs{dt_rows:N} \meta{dt}
%   \end{syntax}
%   Leaves the number of rows in the \meta{data table} in the input
%   stream as an \meta{integer denotation}.
% \end{function}
%
% \begin{function}{\dt_get:NnN}
%   \begin{syntax}
%     \cs{dt_get:NnnN} \meta{dt} \Arg{key} \meta{tl var}
%   \end{syntax}
%   Recovers the \meta{value} stored with \meta{key} from the current row in
%   the \meta{data table}, and places this in the \meta{token list variable}.
%   If the \meta{key} is not found in the \meta{row} of the  \meta{data table}
%   then the \meta{token list variable} will contain the special marker
%   \cs{q_no_value}. The \meta{token list variable} is set within the
%   current \TeX{} group. The \meta{row} should be given as an
%   \meta{integer expression}. See also \cs{dt_get:NnNTF}.
% \end{function}
%
% \begin{function}[TF]{\dt_get:NnN}
%   \begin{syntax}
%     \cs{dt_get:NnnNTF} \meta{dt} \Arg{key} \meta{tl var} \Arg{true code} \Arg{false code}
%   \end{syntax}
%   Recovers the \meta{value} stored with \meta{key} from the current row in
%   the \meta{data table}, and places this in the \meta{token list variable}.
%   If the \meta{key} is not found in the \meta{row} of the  \meta{data table}
%   then the \meta{token list variable} will contain the special marker
%   \cs{q_no_value}. The \meta{token list variable} is set within the
%   current \TeX{} group. The \meta{row} should be given as an
%   \meta{integer expression}. Once the \meta{token list variable} has been
%   assigned either the \meta{true code} or \meta{false code} will be left in
%   the input stream, depending on whether the \meta{key} was found.
%   See also \cs{dt_get:NnN}.
% \end{function}
%
% \begin{function}{\dt_get:NnnN}
%   \begin{syntax}
%     \cs{dt_get:NnnN} \meta{dt} \Arg{row} \Arg{key} \meta{tl var}
%   \end{syntax}
%   Recovers the \meta{value} stored with \meta{key} from \meta{row} in the
%   \meta{data table}, and places this in the \meta{token list variable}. If
%   the \meta{key} is not found in the \meta{row} of the  \meta{data table}
%   then the \meta{token list variable} will contain the special marker
%   \cs{q_no_value}. The \meta{token list variable} is set within the
%   current \TeX{} group. The \meta{row} should be given as an
%   \meta{integer expression}. See also \cs{dt_get:NnnNTF}.
% \end{function}
%
% \begin{function}[TF]{\dt_get:NnnN}
%   \begin{syntax}
%     \cs{dt_get:NnnNTF} \meta{dt} \Arg{row} \Arg{key} \meta{tl var} \Arg{true code} \Arg{false code}
%   \end{syntax}
%   Recovers the \meta{value} stored with \meta{key} from \meta{row} in the
%   \meta{data table}, and places this in the \meta{token list variable}. If
%   the \meta{key} is not found in the \meta{row} of the  \meta{data table}
%   then the \meta{token list variable} will contain the special marker
%   \cs{q_no_value}. The \meta{token list variable} is set within the
%   current \TeX{} group. The \meta{row} should be given as an
%   \meta{integer expression}. Once the \meta{token list variable} has been
%   assigned either the \meta{true code} or \meta{false code} will be left in
%   the input stream, depending on whether the \meta{key} was found.
%   See also \cs{dt_get:NnnN}.
% \end{function}
%
% \section{Mapping to data tables}
%
% \begin{function}{\dt_map_variables:Nnn}
%   \begin{syntax}
%     \cs{dt_map_variables:Nnn} \meta{data table} \Arg{key--variable mapping} \Arg{code}
%   \end{syntax}
%   Applies the \meta{code} to each \meta{row} of the \meta{data table}. The
%   \meta{keys} of the \meta{data table} are mapped to variables by the
%   \meta{key--variable mapping}, which should be a key--value list of the form
%   \begin{verbatim}
%     key-a = \l_a_tl ,
%     key-b = \l_b_tl
%     ...
%   \end{verbatim}
%   It is not necessary to map all of the \meta{keys} in a \meta{data table}
%   to variables. If there is not \meta{value} for a \meta{key} in a row,
%   the variable will contain the marker \cs{q_no_value}. Assignment of the
%   \meta{variables} is local to the current \TeX{} group. The mapping
%   to rows is ordered.
% \end{function}
%
% \begin{variable}{\g_dt_map_level_int}
%   The nesting level of the data table mapping is available as
%   \cs{g_dt_map_level_int}. Within a mapping, the \texttt{int} variable
%   \cs{l_dt_map_\meta{level}_row_int} is available so that the row
%   number being operated on is available. Thus
%   \begin{verbatim}
%     \int_use:c { l_dt_map_ \int_use:N \g_dt_map_level_int _row_int }
%   \end{verbatim}
%   will give the current row for the current mapping.
% \end{variable}
%
% \begin{function}[rEXP]{\dt_map_break:, \dt_map_break:n}
%   \begin{syntax}
%     \cs{dt_map_break:}
%     \cs{dt_map_break:n} \Arg{tokens}
%   \end{syntax}
%   Used to terminate a \cs{dt_map_\ldots} function before all
%   entries in the \meta{data table} have been processed. This will
%   normally take place within a conditional statement, for example
%   \begin{verbatim}
%     \dt_map_variables:Nn \l_my_dt { a = \l_my_tl }
%       {
%         \str_if_eq:VnTF \l_my_tl { bingo }
%           { \dt_map_break: }
%           {
%             % Do something useful
%           }
%       }
%   \end{verbatim}
%   The \texttt{:n} variant will insert the \meta{tokens} into the input stream
%   after the mapping terminates. Use outside of a \cs{dt_map_\ldots} scenario
%   will lead to low level \TeX{} errors.
% \end{function}
%
% \section{Data table conditionals}
%
% \begin{function}[EXP, pTF]{\dt_if_empty:N}
%   \begin{syntax}
%     \cs{dt_if_empty_p:N} \meta{dt}
%     \cs{dt_if_empty:NTF} \meta{dt} \Arg{true code} \Arg{false code}
%   \end{syntax}
%   Tests if the \meta{dt} is empty, containing no keys and no rows.
% \end{function}
%
% \begin{function}[EXP, pTF]{\dt_if_in:Nn}
%   \begin{syntax}
%     \cs{dt_if_in_p:Nn} \meta{dt} \Arg{key}
%     \cs{dt_if_in:NnTF} \meta{dt} \Arg{key} \Arg{true code} \Arg{false code}
%   \end{syntax}
%   Tests if the \meta{key} is present in the \meta{data table} at all,
%   \emph{i.e.} if it is one of the columns of the table. This test will be
%   \texttt{true} even if none of the rows contain an entry for the \meta{key}.
% \end{function}
%
% \begin{function}[EXP, pTF]{\dt_if_in_row:Nnn}
%   \begin{syntax}
%     \cs{dt_if_in_row_p:Nnn} \meta{dt} \Arg{row} \Arg{key}
%     \cs{dt_if_in_row:NnnTF} \meta{dt} \Arg{row} \Arg{key} \Arg{true code} \Arg{false code}
%   \end{syntax}
%   Tests if the \meta{key} is present in the \meta{row} of the
%   \meta{data table}. The \meta{row} may be given as an \meta{integer
%   expression}.
% \end{function}
%
% \begin{function}[EXP, pTF]{\dt_if_in_row:Nn}
%   \begin{syntax}
%     \cs{dt_if_in_row_p:Nn} \meta{dt} \Arg{key}
%     \cs{dt_if_in_row:NnTF} \meta{dt} \Arg{key} \Arg{true code} \Arg{false code}
%   \end{syntax}
%   Tests if the \meta{key} is present in the current row of
%   \meta{data table}.
% \end{function}
%
% \section{Variables}
%
% \begin{variable}{\c_empty_dt}
%   A permanently empty data table.
% \end{variable}
%
% \begin{variable}{\l_tmpa_dt, \l_tmpb_dt, \g_tmpa_dt, \g_tmpb_dt}
%   Scratch data tables for general use: these are never used by the kernel.
% \end{variable}
%
% \end{documentation}
%
% \begin{implementation}
%
% \section{\pkg{l3dt} implementation}
%
%    \begin{macrocode}
%<*initex|package>
%    \end{macrocode}
%
%    \begin{macrocode}
%<@@=dt>
%    \end{macrocode}
%
%    \begin{macrocode}
%<*package>
\ProvidesExplPackage
  {\ExplFileName}{\ExplFileDate}{\ExplFileVersion}{\ExplFileDescription}
%</package>
%    \end{macrocode}
%
% \subsection{Structures}
%
% The structure of a data table must allow each row (record) to contain only
% some of the keys, and for the keys to be removed after the table
% is initialised. It also needs to ensure that a unique match can be made to
% every item in the table. At the same time, it is desirable to keep all of
% the information about the table in a single \TeX{} macro. This can be
% achieved by packing the data into a structure in which each key and row is
% numbered:
% \begin{quote}
%   \Arg{rows} \\
%   \Arg{columns} \\
%   \cs{q_@@} \meta{key_1} \cs{q_@@} \meta{key_2} \cs{q_@@} \ldots \\
%   \cs{q_nil} \\
%   \cs{q_@@_header} \\
%   \cs{q_@@_row} \\
%   \meta{row_1} \\
%   \cs{q_@@} \meta{key_1} \cs{q_@@} \Arg{data_{1,1}} \\
%   \cs{q_@@} \meta{key_2} \cs{q_@@} \Arg{data_{1,2}} \\
%   \ldots \\
%   \cs{q_@@} \\
%   \cs{q_nil} \\
%   \cs{q_@@_row} \\
%   \meta{row_2} \\
%   \cs{q_@@} \meta{key_1} \cs{q_@@} \Arg{data_{2,1}} \\
%   \cs{q_@@} \meta{key_2} \cs{q_@@} \Arg{data_{2,2}} \\
%   \ldots \\
%   \cs{q_@@} \\
%   \cs{q_nil} \\
%   \cs{q_@@_row} \\
%   \ldots \\
%   \cs{q_@@_row}
% \end{quote}
%
% \begin{variable}{\q_@@, \q_@@_row, \q_@@_header}
%   The quarks are set up.
%    \begin{macrocode}
\quark_new:N \q_@@
\quark_new:N \q_@@_row
\quark_new:N \q_@@_header
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}{\c_empty_dt}
%   A permanently-empty data table, which therefore contains only the minimum
%   number of items necessary to comply with the structure above.
%    \begin{macrocode}
\tl_const:Nn \c_empty_dt
  {
    { 0 }
    { 0 }
    \q_@@
    \q_nil
    \q_@@_header
    \q_@@_row
  }
%    \end{macrocode}
% \end{variable}
%
% \subsection{Allocation and initialisation}
%
% \begin{macro}{\dt_new:N}
%   Internally, data tables are token lists, but an empty dt
%   is not an empty tl.
%    \begin{macrocode}
\cs_new_protected:Npn \dt_new:N #1 { \cs_new_eq:NN #1 \c_empty_dt }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\dt_clear:N, \dt_gclear:N}
%   The same idea for  clearing.
%    \begin{macrocode}
\cs_new_protected:Npn \dt_clear:N #1  { \cs_set_eq:NN  #1 \c_empty_dt }
\cs_new_protected:Npn \dt_gclear:N #1 { \cs_gset_eq:NN #1 \c_empty_dt }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}
%   {\dt_clear_new:N, \dt_gclear_new:N}
%   Once again a simple copy from the token list functions.
%    \begin{macrocode}
\cs_new_protected:Npn \dt_clear_new:N #1
  { \cs_if_exist:NTF #1 { \dt_clear:N #1 } { \dt_new:N #1 } }
\cs_new_protected:Npn \dt_gclear_new:N #1
  { \cs_if_exist:NTF #1 { \dt_gclear:N #1 } { \dt_new:N #1 } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\dt_set_eq:NN, \dt_gset_eq:NN}
%   Once again, these are simply copies from the token list functions.
%    \begin{macrocode}
\cs_new_eq:NN \dt_set_eq:NN  \tl_set_eq:NN
\cs_new_eq:NN \dt_gset_eq:NN \tl_gset_eq:NN
%    \end{macrocode}
% \end{macro}
%
% \begin{variable}{\l_tmpa_dt, \l_tmpb_dt, \g_tmpa_dt, \g_tmpb_dt}
%   Scratch tables.
%    \begin{macrocode}
\dt_new:N \l_tmpa_dt
\dt_new:N \l_tmpb_dt
\dt_new:N \g_tmpa_dt
\dt_new:N \g_tmpb_dt
%    \end{macrocode}
% \end{variable}
%
% \subsection{Splitting functions}
%
% \begin{macro}[aux]{\@@_split:nnnn}
% \begin{macro}[aux]{\@@_split:w}
%   Two general auxiliaries. The \texttt{nnnn} function is used to apply the
%   \texttt{T} branch if a match is found and the \texttt{F} branch otherwise.
%   The \texttt{w} function is general purpose, and is used to define the
%   matching parameter set.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_split:nnnn #1#2#3#4 { #3 #2 }
\cs_new_protected:Npn \@@_split:w { }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}[int,EXP]{\@@_split_header:NT}
% \begin{macro}[aux,EXP]{\@@_split_header:wn}
%   Splits the header from the table, inserting the code required to then
%   process the split table. The \cs{q_nil} is also removed from the end of the
%   header, as it is essentially a distraction here.
%    \begin{macrocode}
\cs_new:Npn \@@_split_header:NT #1#2
  { \exp_after:wN \@@_split_header:wn #1 \q_stop {#2} }
\cs_new:Npn \@@_split_header:wn #1 \q_nil \q_@@_header #2 \q_stop #3
  { #3 {#1} { \q_@@_header #2 } }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}[int]{\@@_split_key:nnTF}
% \begin{macro}[aux]{\@@_split_key_aux:nnTF}
%   Here, the split is made for a partial list within a row. The row is
%   basically the same as a property list, so the split here is almost
%   identical to that in \cs{prop_split_aux:NnTF}. The row-end data is set up
%   such that it will not interfere with this process.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_split_key:nnTF #1#2
  { \exp_args:No \@@_split_key_aux:nnTF { \tl_to_str:n {#2} } {#1} }
\cs_new_protected:Npn \@@_split_key_aux:nnTF #1#2
  {
    \cs_set_protected:Npn \@@_split:w
      ##1 \q_@@ #1 \q_@@ ##2##3##4 \q_mark ##5 \q_stop
      { \@@_split:nnnn ##3 { { ##1 \q_@@ } {##2} {##4} } }
    \@@_split:w #2 \q_mark
      \q_@@ #1 \q_@@ { } { ? \use_ii:nn { } } \q_mark \q_stop
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}[int]{\@@_split_key_list:NnTF}
% \begin{macro}[aux]{\@@_split_key_list_aux:NnTF}
%   Finding a key in the header uses a similar approach to finding a key in
%   a property list. Here, if the key is found there will always be at least
%   one token between \cs{q_@@} and \cs{q_@@_header} due to the \cs{q_nil}
%   which is part of a new table. The use of |##1##2##3| in \cs{@@_split:w}
%   here is to deal with the overall number of rows and keys. The set up here
%   means that these will always be unbraced then rebraced: simply grabbing
%   |##1##2| to include this and anything before the key of interest will give
%   variable results depending on whether the match is to the very first key or not.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_split_key_list:NnTF #1#2
  { \exp_args:NNo \@@_split_key_list_aux:NnTF #1 { \tl_to_str:n {#2} } }
\cs_new_protected:Npn \@@_split_key_list_aux:NnTF #1#2
  {
    \cs_set_protected:Npn \@@_split:w
      ##1##2##3 \q_@@ #2 \q_@@ ##4##5 \q_@@_header ##6 \q_mark ##7 \q_stop
      {
        \@@_split:nnnn ##4
          { { {##1} {##2} ##3 \q_@@ } { ##4##5 \q_@@_header ##6 } }
      }
    \exp_after:wN \@@_split:w #1 \q_mark
      \q_@@ #2 \q_@@ { ? \use_ii:nn { } } \q_@@_header \q_mark \q_stop
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}[int]{\@@_split_row:NnTF}
% \begin{macro}[aux]{\@@_split_row_aux:NnTF, \@@_split_row_aux:NfTF}
%   The usual approach, here using the fact that each row start with row number
%   and ends with \cs{q_nil} so there will always be at least one token to be
%   absorbed as |##2|. The only odd thing to watch here is that the row
%   number is evaluated so that higher-level functions in the main do not
%   need to have an \texttt{f}-type variant.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_split_row:NnTF #1#2
  { \@@_split_row_aux:NfTF #1 { \int_eval:n {#2} } }
\cs_new_protected:Npn \@@_split_row_aux:NnTF #1#2
  {
    \cs_set_protected:Npn \@@_split:w
      ##1 \q_@@_row #2 \q_@@ ##2##3 \q_@@_row ##4 \q_mark ##5 \q_stop
      {
        \@@_split:nnnn ##2
          { { ##1 \q_@@_row } { #2 \q_@@ ##2##3 } {##4} }
      }
    \exp_after:wN \@@_split:w #1 \q_mark
      \q_@@_row #2 \q_@@ { ? \use_ii:nn { } } \q_@@_row \q_mark \q_stop
  }
\cs_generate_variant:Nn \@@_split_row_aux:NnTF { Nf }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{Adding and removing data}
%
% \begin{macro}{\dt_add_key:Nn, \dt_gadd_key:Nn}
% \begin{macro}[aux]{\@@_add_key:NNn}
% \begin{macro}[aux]{\@@_add_key:NNnnn}
% \begin{macro}[aux]{\@@_add_key:NNnnn}
%   Here, there are two stages. If the key is already present in the list of
%   known keys then no action is taken, and the split list is thrown away.
%   On the other hand, if the key is not present then the header and body
%   are separated and the key is added to the end of the list of known keys
%   (hence keys are ordered). The \cs{@@_split_header:Nn} function will have
%   removed the \cs{q_nil} from the header, and so it is put back in here.
%    \begin{macrocode}
\cs_new_protected_nopar:Npn \dt_add_key:Nn { \@@_add_key:NNn \tl_set:Nx }
\cs_new_protected_nopar:Npn \dt_gadd_key:Nn { \@@_add_key:NNn \tl_gset:Nx }
\cs_new_protected:Npn \@@_add_key:NNn #1#2#3
  {
    \@@_split_key_list:NnTF #2 {#3}
      { \use_none:nn }
      {
        \@@_split_header:NT #2
          { \@@_add_key:NNnnn #1 #2 {#3} }
      }
  }
\cs_new_protected:Npn \@@_add_key:NNnnn #1#2#3#4#5
  { \@@_add_key:NNnnnwnn #1 #2 #4 \q_stop {#3} {#5} }
\cs_new_protected:Npn \@@_add_key:NNnnnwnn #1#2#3#4#5 \q_stop #6#7
  {
    #1 #2
      {
        {#3}
        { \int_eval:n { #4 + \c_one } }
        \exp_not:n {#5}
        \tl_to_str:n {#6}
        \exp_not:n { \q_@@ \q_nil #7 }
      }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\dt_add_row:N, \dt_gadd_row:N}
% \begin{macro}[aux]{\@@_add_row:NN}
% \begin{macro}[aux]{\@@_add_row:NnN, \@@_add_row:NfN}
% \begin{macro}[aux,EXP]{\@@_add_row:nw}
%   Adding a row means incrementing the total number and adding the structure
%   of an empty row. As finding the rows will get slow for large tables, this
%   is only done once.
%    \begin{macrocode}
\cs_new_protected_nopar:Npn \dt_add_row:N { \@@_add_row:NN \tl_set:Nx }
\cs_new_protected_nopar:Npn \dt_gadd_row:N { \@@_add_row:NN \tl_gset:Nx }
\cs_new_protected:Npn \@@_add_row:NN #1#2
  { \@@_add_row:NfN #1 { \int_eval:n { \dt_rows:N #2 + \c_one } } #2 }
\cs_new_protected:Npn \@@_add_row:NnN #1#2#3
  {
    #1 #3
      {
        {#2}
        \exp_after:wN \@@_add_row:nw #3 \q_stop
        #2
        \exp_not:n { \q_@@ \q_nil \q_@@_row }
      }
  }
\cs_generate_variant:Nn \@@_add_row:NnN { Nf } 
\cs_new:Npn \@@_add_row:nw #1#2 \q_stop { \exp_not:n {#2} }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\dt_put:Nnn, \dt_gput:Nnn}
%   Adding to the current row is simply a special case of adding to an
%   arbitrary row.
%    \begin{macrocode}
\cs_new_protected:Npn \dt_put:Nnn #1
  { \dt_put:Nnnn #1 { \dt_rows:N #1 } }
\cs_new_protected:Npn \dt_gput:Nnn #1
  { \dt_gput:Nnnn #1 { \dt_rows:N #1 } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\dt_put:Nnnn, \dt_gput:Nnnn}
% \begin{macro}[aux]{\@@_put:NNNnnn}
% \begin{macro}[aux]{\@@_put:NNnnnnn}
% \begin{macro}[aux]{\@@_put_update:NNnnnnnnn}
% \begin{macro}[aux]{\@@_put_add_to_row:NNnnnnn}
% \begin{macro}[aux, EXP]{\@@_put_add_to_row_aux:w}
%   Adding to a row is a slightly complex procedure. The lead-off is the
%   standard combination across the local and global routes.
%    \begin{macrocode}
\cs_new_protected_nopar:Npn \dt_put:Nnnn
  { \@@_put:NNNnnn \dt_add_key:Nn \tl_set:Nx }
\cs_new_protected_nopar:Npn \dt_gput:Nnnn
  { \@@_put:NNNnnn \dt_gadd_key:Nn \tl_gset:Nx }
%    \end{macrocode}
%   Add the key to the list those known, if necessary, then check that the
%   row requested makes sense.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_put:NNNnnn #1#2#3#4#5#6
  {
    #1 #3 {#5}
    \@@_split_row:NnTF #3 {#4}
      { \@@_put:NNnnnnn #2 #3 {#5} {#6} }
      {
        \__msg_kernel_error:nnxxx { dt } { unknown-row }
          { \token_to_str:N #3 } { \int_eval:n {#4} } { \dt_rows:N #3 }
      }
  }
%    \end{macrocode}
%   At this stage, the arguments are
%   \begin{enumerate}
%     \item the set function \cs{tl_(g)set:Nx},
%     \item the data table,
%     \item the key,
%     \item the value,
%     \item the data table before the row,
%     \item the extracted data table row,
%     \item the data table after the row.
%   \end{enumerate}
%   Splitting on the key will then leave three further items in the input
%   stack if the key is already present. So there is some care needed sending
%   the parameters forward without running out of \TeX{} arguments.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_put:NNnnnnn #1#2#3#4#5#6#7
  {
    \@@_split_key:nnTF {#6} {#3}
      { \@@_put_update:NNnnnnnnn #1 #2 {#3} {#4} {#5} {#7} }
      { \@@_put_add_to_row:NNnnnnn #1 #2 {#3} {#4} {#5} {#6} {#7} }
  }
%    \end{macrocode}
%   The arguments here are
%   \begin{enumerate}
%     \item the set function \cs{tl_(g)set:Nx},
%     \item the data table,
%     \item the key,
%     \item the value,
%     \item the data table before the row,
%     \item the data table after the row,
%     \item the row before the key,
%     \item the current value for the key
%     \item the row after the key.
%   \end{enumerate}
%   What happens here is a reconstruction of the table: everything except
%   |#8| is needed. To try to keep things clear, there are a few more
%   \cs{exp_not:n} here than formally required.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_put_update:NNnnnnnnn #1#2#3#4#5#6#7#8#9
  {
    #1 #2
      {
        \exp_not:n { #5 #7 }
        \tl_to_str:n {#3}
        \exp_not:n { \q_@@ {#4} \q_@@ #9 \q_@@_row #6 }
      }
  }
%    \end{macrocode}
%   A slightly more complex case when adding an item. The arguments here are
%   identical to those for \cs{@@_put:NNnnnnnn}. The row has not been
%   split, so the \cs{q_nil} there is removed and re-added to come after the
%   new content.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_put_add_to_row:NNnnnnn #1#2#3#4#5#6#7
  {
    #1 #2
      {
        \exp_not:n {#5}
        \exp_not:o { \@@_put_add_to_row_aux:w #6 }
        \tl_to_str:n {#3}
        \exp_not:n { \q_@@ {#4} \q_@@ \q_nil \q_@@_row #7 }
      }
  }
\cs_new:Npn \@@_put_add_to_row_aux:w #1 \q_nil {#1}
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}[EXP]{\dt_keys:N}
% \begin{macro}[EXP, aux]{\@@_keys:nnw}
% \begin{macro}[EXP]{\dt_rows:N}
%  The number of rows in a dt is the very first entry. Getting the number of
%  keys is almost the same, except a custom auxiliary is needed.
%    \begin{macrocode}
\cs_new:Npn \dt_keys:N #1 { \exp_after:wN \@@_keys:nnw #1 \q_stop }
\cs_new:Npn \@@_keys:nnw #1#2#3 \q_stop {#2}
\cs_new:Npn \dt_rows:N #1
  { \exp_after:wN \use_i_delimit_by_q_stop:nw #1 \q_stop }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \subsection{Removing data}
%
% \begin{macro}{\dt_remove:Nn,\dt_gremove:Nn}
%   Deleting to the current row is simply a special case of deleting to an
%   arbitrary row.
%    \begin{macrocode}
\cs_new_protected:Npn \dt_remove:Nn #1
  { \dt_remove:Nnn #1 { \dt_rows:N #1 } }
\cs_new_protected:Npn \dt_gremove:Nn #1
  { \dt_gremove:Nnn #1 { \dt_rows:N #1 } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\dt_remove:Nnn, \dt_gremove:Nnn}
% \begin{macro}[aux]{\dt_remove_aux:NNnn}
% \begin{macro}[aux]{\dt_remove_aux:NNnnnn}
% \begin{macro}[aux]{\dt_remove_aux:NNnnnnn}
%   Deleting a single entry from a single row means first splitting by row,
%   then splitting by key, and finally doing the assignment. If the row or the
%   key are not present then the entire function does nothing at all.
%    \begin{macrocode}
\cs_new_protected_nopar:Npn \dt_remove:Nnn { \dt_remove_aux:NNnn \tl_set:Nn }
\cs_new_protected_nopar:Npn \dt_gremove:Nnn { \dt_remove_aux:NNnn \tl_gset:Nn }
\cs_new_protected:Npn \dt_remove_aux:NNnn #1#2#3#4
  {
    \@@_split_row:NnTF #2 {#3}
      { \dt_remove_aux:NNnnnn #1 #2 {#4} }
      { }
  }
\cs_new_protected:Npn \dt_remove_aux:NNnnnn #1#2#3#4#5#6
  {
    \@@_split_key:nnTF {#5} {#3}
      { \dt_remove_aux:NNnnnnn #1 #2 {#4} {#6} }
      { }
  }
\cs_new_protected:Npn \dt_remove_aux:NNnnnnn #1#2#3#4#5#6#7
  { #1 #2 { #3 #5 #7 #4 } }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\dt_remove_key:Nn, \dt_gremove_key:Nn}
% \begin{macro}[aux]{\dt_remove_key_aux:NNn}
% \begin{macro}[aux]{\dt_remove_key_aux:nNNnn}
% \begin{macro}[aux, EXP]{\dt_remove_key_aux:w}
%   Deleting a key also removes from the table itself, so that there is no
%   need to do any awkward checks when extracting data from the table. (It's
%   likely that there will be more cases of accessing data than deleting
%   rows). The deletion mapping ignores rows entirely and just pulls out
%   matching key--value pairs, as this reduces the number of matches needed
%   to a minimum.
%    \begin{macrocode}
\cs_new_protected_nopar:Npn \dt_remove_key:Nn
  { \dt_remove_key_aux:NNn \tl_set:Nx }
\cs_new_protected_nopar:Npn \dt_gremove_key:Nn
  { \dt_remove_key_aux:NNn \tl_gset:Nx }
\cs_new_protected:Npn \dt_remove_key_aux:NNn #1#2#3
  {
    \@@_split_key_list:NnTF #2 {#3}
      { \exp_args:No \dt_remove_key_aux:nNNnn { \tl_to_str:n {#3} } #1 #2 }
      { }
  }
\cs_new_protected:Npn \dt_remove_key_aux:nNNnn #1#2#3#4#5
  { \dt_remove_key_aux:nNNnnwn {#1} #2 #3 #4 \q_stop {#5} }
\cs_new_protected:Npn \dt_remove_key_aux:nNNnnwn #1#2#3#4#5#6 \q_stop #7
  {
    \cs_set:Npn \dt_remove_key_aux:w ##1 \q_@@ #1 \q_@@ ##2 ##3
      {
        \exp_not:n {##1}
        \__quark_if_recursion_tail_break:nN {##3} \dt_map_break:
        \dt_remove_key_aux:w ##3
      }

    #2 #3
      {
        {#4}
        { \int_eval:n { #5 - \c_one } }
        \exp_not:n {#6}
        \dt_remove_key_aux:w #7 \q_@@ #1 \q_@@ { } \q_recursion_tail
          \__prg_break_point:Nn \dt_map_break: { }
      }
  }
\cs_new:Npn \dt_remove_key_aux:w { }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\dt_remove_row:Nn, \dt_gremove_row:Nn}
% \begin{macro}[aux]{\dt_remove_row_aux:NNn}
% \begin{macro}[aux]{\dt_remove_row_aux:NNnnnn}
% \begin{macro}[aux, EXP]{\dt_remove_row_aux:nw}
% \begin{macro}[aux, EXP]{\dt_remove_row_loop:nw}
%   Removing a row is a slightly complex operation as there are two stages.
%   The row itself is easy enough to remove, but then all later rows have to
%   be renumbers.
%    \begin{macrocode}
\cs_new_protected_nopar:Npn \dt_remove_row:Nn
  { \dt_remove_row_aux:NNn \tl_set:Nx }
\cs_new_protected_nopar:Npn \dt_gremove_row:Nn
  { \dt_remove_row_aux:NNn \tl_gset:Nx }
\cs_new_protected:Npn \dt_remove_row_aux:NNn #1#2#3
  {
    \@@_split_row:NnTF #2 {#3}
      { \dt_remove_row_aux:NNnnn #1 #2 }
      { }
  }
%    \end{macrocode}
%   If the code gets here, then |#3| is the table before the removed row,
%   |#4| is the removed row and |#5| is everything afterwards. The first stage
%   is to work out the new number of rows, then include all of |#3| except
%   the old number of rows. The removed row |#4| is thrown away, and then there
%   is a loop to recalculate the row numbers for all of the later rows.
%    \begin{macrocode}
\cs_new_protected:Npn \dt_remove_row_aux:NNnnn #1#2#3#4#5
  {
    #1 #2
      {
        { \int_eval:n { \dt_rows:N #2 - \c_one } }
        \dt_remove_row_aux:nw #3 \q_stop
        \dt_remove_row_loop:nw #5 \q_recursion_tail \q_@@_row
          \__prg_break_point:Nn \dt_map_break: { }
      }
  }
\cs_new_eq:NN \dt_remove_row_aux:nw \@@_add_row:nw
\cs_new:Npn \dt_remove_row_loop:nw #1#2 \q_@@_row
  {
    \__quark_if_recursion_tail_break:nN {#1} \dt_map_break:
    \int_eval:n { #1 - \c_one }
    \exp_not:n { #2 \q_@@_row }
    \dt_remove_row_loop:nw
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \subsection{Accessing data in data tables}
%
% \begin{macro}{\dt_get:NnnN}
% \begin{macro}[aux]{\dt_get_aux:nNnnn}
% \begin{macro}[aux]{\dt_get_aux:nNnnn}
%   Recovering a value from a row means doing two splits: first find the row,
%   then find the key. Nothing exciting, just a question of tracking the
%   returned items.
%    \begin{macrocode}
\cs_new_protected:Npn \dt_get:NnnN #1#2#3#4
  {
    \@@_split_row:NnTF #1 {#2}
      { \dt_get_aux:nNnnn {#3} #4 }
      { \tl_set:Nn #4 { \q_no_value } }
  }
\cs_new_protected:Npn \dt_get_aux:nNnnn #1#2#3#4#5
  {
    \@@_split_key:nnTF {#4} {#1}
      { \dt_get_aux:Nnnn #2 }
      { \tl_set:Nn #2 { \q_no_value } }
  }
\cs_new_protected:Npn \dt_get_aux:Nnnn #1#2#3#4 { \tl_set:Nn #1 {#3} }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}[TF]{\dt_get:NnnN}
% \begin{macro}[aux]{\@@_get_true:nNnnn}
% \begin{macro}[aux]{\@@_get_true:Nnnn}
%   The same idea as the standard method, but built as a conditional.
%    \begin{macrocode}
\prg_new_protected_conditional:Npnn \dt_get:NnnN #1#2#3#4 { T , F , TF }
  {
    \@@_split_row:NnTF #1 {#2}
      { \@@_get_true:nNnnn {#3} #4 }
      { \prg_return_false: }
  }
\cs_new_protected:Npn \@@_get_true:nNnnn #1#2#3#4#5
  {
    \@@_split_key:nnTF {#4} {#1}
      { \@@_get_true:Nnnn #2 }
      { \prg_return_false: }
  }
\cs_new_protected:Npn \@@_get_true:Nnnn #1#2#3#4
  {
    \tl_set:Nn #1 {#3}
    \prg_return_true:
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\dt_get:NnN}
% \begin{macro}[TF]{\dt_get:NnN}
%   Simple wrappers.
%    \begin{macrocode}
\cs_new_protected:Npn \dt_get:NnN #1
  { \dt_get:NnnN  #1 { \dt_rows:N #1 } }
\cs_new_protected:Npn \dt_get:NnNT #1
  { \dt_get:NnnNF  #1 { \dt_rows:N #1 } }
\cs_new_protected:Npn \dt_get:NnNF #1
  { \dt_get:NnnNF  #1 { \dt_rows:N #1 } }
\cs_new_protected:Npn \dt_get:NnNTF #1
  { \dt_get:NnnNTF #1 { \dt_rows:N #1 } }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{Mapping to data tables}
%
% \begin{variable}{\g_dt_map_level_int}
%   Unlike other mappings, the mapping level here has to be available
%   and so linked to the module.
%    \begin{macrocode}
\int_new:N \g_dt_map_level_int
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}{\dt_map_variables:Nnn}
% \begin{macro}[aux]{\@@_map_variables_key:nn}
% \begin{macro}[aux]{\@@_map_variables:nnn}
% \begin{macro}[aux]{\@@_map_variables:nNNw}
% \begin{macro}[aux]{\@@_map_variables:nnw}
%   Mapping across a data table is more complex than other cases as there
%   are two \enquote{dimensions} to worry about: the rows and the keys.
%   The first stage of the mapping is to convert the key--variable mapping
%   into a sequence that can be used later. This is done with the assumption
%   that any key without a variable can simply be dropped entirely. The
%   header of the table is then split from the body.
%    \begin{macrocode}
\cs_new_protected:Npn \dt_map_variables:Nnn #1#2#3
  {
    \int_gincr:N \g_dt_map_level_int
    \seq_gclear_new:c { g_dt_map_ \int_use:N \g_dt_map_level_int _seq }
    \keyval_parse:NNn \use_none:n \@@_map_variables_key:nn {#2}
    \@@_split_header:NT #1 { \@@_map_variables:nnn {#3} }
  }
\cs_new_protected:Npn \@@_map_variables_key:nn #1#2
  {
    \seq_gput_right:cn { g_dt_map_ \int_use:N \g_dt_map_level_int _seq }
      { {#1} #2 }
  }
%    \end{macrocode}
%   As \cs{@@_split_header:NT} will leave a couple of tokens at the front
%   of the body part of the split, there is a quick piece of tidying up
%   to remove them.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_map_variables:nnn #1#2#3
  { \@@_map_variables:nNNw {#1} #3 \q_stop }
\cs_new_protected:Npn \@@_map_variables:nNNw
  #1 \q_@@_header \q_@@_row #2 \q_stop
  {
    \int_zero_new:c { l_dt_map_ \int_use:N \g_dt_map_level_int _row_int }
    \@@_map_variables:nnw {#1} #2 { } \q_recursion_tail \q_@@_row
      \__prg_break_point:Nn \dt_map_break:
        { \int_gdecr:N \g_dt_map_level_int }
  }
\cs_new_protected:Npn \@@_map_variables:nnw #1#2#3#4 \q_@@_row
  {
    \__quark_if_recursion_tail_break:nN {#3} \dt_map_break:
    \seq_map_inline:cn { g_dt_map_ \int_use:N \g_dt_map_level_int _seq }
      { \dt_get_aux:nNnnn ##1 { } {#3#4} { } }
    #1
    \int_incr:c { l_dt_map_ \int_use:N \g_dt_map_level_int _row_int }
    \@@_map_variables:nnw {#1}
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}[rEXP]{\dt_map_break:}
% \begin{macro}[rEXP]{\dt_map_break:n}
%   The break statements use the general \cs{__prg_map_break:Nn}.
%    \begin{macrocode}
\cs_new_nopar:Npn \dt_map_break:
  { \__prg_map_break:Nn \dt_map_break: { } }
\cs_new_nopar:Npn \dt_map_break:n
  { \__prg_map_break:Nn \dt_map_break: }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{Data table conditionals}
%
% \begin{macro}[pTF, EXP]{\dt_if_empty:N}
%   An empty data table has not only no rows but also no keys. (The number of
%   rows can be tested using \cs{dt_rows:N} and an \texttt{int} test.)
%    \begin{macrocode}
\prg_new_conditional:Npnn \dt_if_empty:N #1 { T , F , TF , p }
  {
    \if_meaning:w #1 \c_empty_dt
      \prg_return_true:
    \else:
      \prg_return_false:
    \fi:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[pTF, EXP]{\dt_if_in:Nn}
% \begin{macro}[aux, EXP]{\@@_if_in:nnn}
% \begin{macro}[aux, EXP]{\@@_if_in:nwN}
% \begin{macro}[aux, EXP]{\@@_if_in:n}
%   Expandably checking for the presence of a key in the table as a whole
%   requires a mapping to the header. The idea is the usual recursion set
%   up with a string-based comparison only after checking for the end of
%   the loop.
%    \begin{macrocode}
\prg_new_conditional:Npnn \dt_if_in:Nn #1#2 { p , T , F , TF }
  { \@@_split_header:NT #1 { \@@_if_in:nnn {#2} } }
\cs_new:Npn \@@_if_in:nnn #1#2#3
  {
    \exp_last_unbraced:Nno \@@_if_in:nwN {#1} { \use_none:nn #2 }
      \q_recursion_tail \q_@@
      \__prg_break_point:
  }
\cs_new:Npn \@@_if_in:nwN #1#2 \q_@@
  {
    \if_meaning:w \q_recursion_tail #2
      \exp_after:wN \__prg_break:n
    \else:
      \exp_after:wN \use_none:n
    \fi:
      { \prg_return_false: }
    \str_if_eq:nnTF {#1} {#2}
      { \__prg_break:n { \prg_return_true: } }
      { \@@_if_in:nwN {#1} }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}[pTF, EXP]{\dt_if_in_row:Nnn}
% \begin{macro}[aux, EXP]{\@@_if_in_row:nw}
% \begin{macro}[aux, EXP]{\@@_if_in_row:nn}
% \begin{macro}[aux, EXP]{\@@_if_in_row:nwn}
% \begin{macro}[aux, EXP]{\@@_if_in_row:N}
%   Finding a key in a single row in an expandable way requires two mappings.
%   To start of with, there is a search for the row. This uses for termination
%   the fact that each row starts \cs{q_@@_row} and ends \cs{q_nil}, and
%   always contains at least the row number as the first \meta{balanced
%   text}. That can be replaced by the tail marker to terminate iteration:
%   all that is then needed is the correct placement of the clean-up code.
%    \begin{macrocode}
\prg_new_conditional:Npnn \dt_if_in_row:Nnn #1#2#3 { p , T , F , TF }
  {
    \exp_last_unbraced:Nno \@@_if_in_row:nw {#2} #1
      \q_recursion_tail \q_nil
      \__prg_break_point:
      { \tl_to_str:n {#3} }
  }
%    \end{macrocode}
%   The row iteration does a numerical comparison to see if the target row has
%   been found. That means that the row argument does not need to be converted
%   to a number earlier.
%    \begin{macrocode}
\cs_new:Npn \@@_if_in_row:nw #1#2 \q_@@_row #3#4 \q_nil
  {
    \if_meaning:w \q_recursion_tail #3
      \exp_after:wN \__prg_break:n
    \else:
      \exp_after:wN \use_none:n
    \fi:
      {
        \use_i:nn
        \prg_return_false:
      }
    \int_compare:nNnTF {#1} = {#3}
      { \__prg_break:n { \exp_args:Nno \@@_if_in_row:nn {#4} } }
      { \@@_if_in_row:nw {#1} }
  }
%    \end{macrocode}
%   The second iteration is along the row. This is basically the same as
%   \cs{prop_if_in:NnTF} with the \cs{q_@@} in place of \cs{q__prop}.
%    \begin{macrocode}
\cs_new:Npn \@@_if_in_row:nn #1#2
  {
    \@@_if_in_row:nwn {#2} #1 {#2} \q_@@ { } \q_recursion_tail
      \__prg_break_point:
  }
\cs_new:Npn \@@_if_in_row:nwn #1 \q_@@ #2 \q_@@ #3
  {
    \str_if_eq_x:nnTF {#1} {#2}
      { \@@_if_in_row:N }
      { \@@_if_in_row:nwn {#1} }
  }
\cs_new:Npn \@@_if_in_row:N #1
  {
    \if_meaning:w \q_@@ #1
      \prg_return_true:
    \else:
      \prg_return_false:
    \fi:
    \__prg_break:
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}[pTF, EXP]{\dt_if_in_row:Nn}
%   Simple wrappers.
%    \begin{macrocode}
\cs_new:Npn \dt_if_in_row_p:Nn #1
  { \dt_if_in_row_p:Nnn #1 { \dt_rows:N #1 } }
\cs_new:Npn \dt_if_in_row:NnT  #1
  { \dt_if_in_row:NnnT  #1 { \dt_rows:N #1 } }
\cs_new:Npn \dt_if_in_row:NnF  #1
  { \dt_if_in_row:NnnF  #1 { \dt_rows:N #1 } }
\cs_new:Npn \dt_if_in_row:NnTF #1
  { \dt_if_in_row:NnnTF #1 { \dt_rows:N #1 } }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Messages}
%
%    \begin{macrocode}
\__msg_kernel_new:nnnn { dt } { unknown-row }
  { Data~table~#1~does~not~contain~a~row~'#2'. }
  {
    Data~table~#1~contains~#3~rows.~These~must~be~accessed~by~number:~row~
    #2~is~not~present~in~the~table.
  }
%    \end{macrocode}
%
%    \begin{macrocode}
%</initex|package>
%    \end{macrocode}
%
% \end{implementation}
%
% \PrintIndex
