23

I'm defining a list and I need to obtain the n-th element of this list. I was surprised that etoolbox doesn't provide a way to obtain it. The way I found to obtain the n-th element is along these lines:

\documentclass{minimal}

\usepackage{etoolbox}

\newcounter{mylistcounter}

\def\mylist{}
\forcsvlist{\listadd\mylist}{%
   first element,
   second element,
   third element,
   fourth element,
   fifth element
}%

\def\getnthelement#1{%
   \setcounter{mylistcounter}{1}%
   \renewcommand*\do[1]{%
      \ifnumequal{\value{mylistcounter}}{#1}{##1\listbreak}\relax
      \stepcounter{mylistcounter}%
   }%
   \dolistloop{\mylist}
}%

\begin{document}
\def\fourthelement{\getnthelement{4}} % <--- Fine!
% \edef\fourthelement{\getnthelement{4}} % <--- Problem!

\begin{itemize}
\item The third element is: ``\getnthelement{3}''.
\item The fourth element is: ``\fourthelement''.
\item The fourth element again: ``\fourthelement''.
\item The fifth element is: ``\getnthelement{5}''.
\end{itemize}
\end{document}

Unfortunately, I need the same element several times, and I don't want to loop through the list each time, so I need to somewhat save it somewhere. I thought this would be achieved by using an \edef, but as you may see in the MWE above, with the \edef uncommented, I obtain the error:

! Undefined control sequence.
\GenericError  ...
                                                    #4  \errhelp \@err@     ...
l.27 \edef\fourthelement{\getnthelement{4}
                                              } % <--- Problem!
? h
The control sequence at the end of the top line
of your error message was never \def'ed. If you have
misspelled it (e.g., `\hobx'), type `I' and the correct
spelling (e.g., `I\hbox'). Otherwise just continue,
and I'll forget about whatever was undefined.

?

Any idea on how I could solve this problem? Any solution welcome, even solutions not using etoolbox. But please avoid non-stable packages. Also, I need a somewhat general method, since I need to use all the elements of the list several times, but always in the same shot.

7 Answers7

21

If you need repeated access to arbitrary items then an "array" of command names \mylist1, \mylist2\ ... might be more suitable than a list.

\documentclass{minimal}

\usepackage{etoolbox}

\newcounter{mylistcounter}

\def\saveitem#1{%
\stepcounter{mylistcounter}%
\expandafter\def\csname mylist\themylistcounter\endcsname{#1}}

\forcsvlist{\saveitem}{%
   first element,
   second element,
   third element,
   fourth element,
   fifth element
}%


\def\getnthelement#1{\csname mylist#1\endcsname}


\begin{document}


\begin{itemize}
\item The third element is: ``\getnthelement{3}''.
\item The fourth element is: ``\getnthelement{4}''.
\item The fourth element again: ``\getnthelement{4}''.
\item The fifth element is: ``\getnthelement{5}''.
\end{itemize}
\end{document}
David Carlisle
  • 757,742
  • Accepting this answer. It works flawlessly and my colleagues will be happy colleagues! – gniourf_gniourf Nov 02 '12 at 19:18
  • Remark, for large array there might be performance implication because of hash collision → macros - How to implement (low-level) arrays in TeX - TeX - LaTeX Stack Exchange – user202729 Aug 23 '22 at 14:25
  • @user202729 yes the csame hash in tex isn't state of the art compared to other possible hash that could have been used in the engine code, but in practice it is likely faster than any lookup coded within the tex macro layer, even if all names collide and so it degenerates to a linear search, a linear search in pascal/C is likely faster than anything coded in TeX for any list length likely to appear in a document – David Carlisle Aug 23 '22 at 14:34
  • I mean yes obviously unless you use toks register you clearly need at least O(n/s) hash entries to get O(s+log n) lookup performance, just saying the user may want to implement the tricks there to avoid some hash collision for extra performance (actually just notice this question is bumped, but anyway for future user reference) – user202729 Aug 23 '22 at 15:31
11

Here's a possible solution with xparse:

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\newgniourflist}{ m }
 {
  \seq_new:c { g_gniourf_#1_seq }
 }
\newgniourflist{gniourflist}
\NewDocumentCommand{\addtogniourflist}{ O{gniourflist} m }
 {
  \seq_gput_right:cn { g_gniourf_#1_seq } { #2 }
 }
\NewExpandableDocumentCommand{\getnthelement}{ O{gniourflist} m }
 {
  \seq_item:cn { g_gniourf_#1_seq } { #2 }
 }
\NewDocumentCommand{\storenthelement}{ O{gniourflist} m m }
 {
  \cs_set:Npx #3 { \seq_item:cn { g_gniourf_#1_seq } { #2 } }
 }
\NewDocumentCommand{\cleargniourflist}{ O{gniourflist} }
 {
  \seq_gclear:c { g_gniourf_#1_seq }
 }
\ExplSyntaxOff

\newcommand{\fourthelement}{\getnthelement{4}}

\begin{document}

\addtogniourflist{first element}
\addtogniourflist{second element}
\addtogniourflist{third element}
\addtogniourflist{fourth element}
\addtogniourflist{fifth element}

\begin{itemize}
\item The third element is: ``\getnthelement{3}''.
\item The fourth element is: ``\fourthelement''.
\item The fourth element again: ``\fourthelement''.
\item The fifth element is: ``\getnthelement{5}''.
\end{itemize}

\storenthelement{4}{\playaroundelement}

\texttt{\meaning\playaroundelement}

\end{document}

The last lines show that the macro \playaroundelement is defined to expand just to fourth element.

With these macros you can manage more than one list; the default one is called gniourflist; if you want to clear it, you simply issue \cleargniourflist. But you can say

\newgniourflist{anotherlist}

and use the newlist as before, just adding an optional argument:

\addtogniourflist[anotherlist]{something}
\getnthelement[anotherlist]{1}
\storenthelement[anotherlist]{1}{\someelement}
\cleargniourflist[anotherlist]
egreg
  • 1,121,712
  • Wow, LaTeX3 stuff... I'm having errors when compiling: ! Undefined control sequence. \getnthelement #1#2->\seq_item:cn – gniourf_gniourf Nov 02 '12 at 18:46
  • @gniourf_gniourf I'm not; update your TeX distribution: the key function \seq_item:cn is a recent addition. – egreg Nov 02 '12 at 18:47
  • Oh, okay... I'm writing a small package to be shared with colleagues (to type exercise sheets), and I can't afford asking them to update their TeX distribution (most of them work on Windows XP and are already not very fond of LaTeX... they'll once again whine and say that MS Words is better). – gniourf_gniourf Nov 02 '12 at 18:51
  • @gniourf_gniourf Sorry. But surely they are happily updating their word processing application, paying what is due, because otherwise they can't send documents to each other any more when one of them does the update. :( – egreg Nov 02 '12 at 18:55
  • Yes, you are right, they are! Everything you say here is, unfortunately, absolutely true! – gniourf_gniourf Nov 02 '12 at 18:58
  • I'm using this code to use some lists. What would be the best way to iterate through such a list? – Marc Sharma Dec 09 '16 at 18:33
  • @MarcSharma Not really clear what you're asking. Since we're using sequences, \seq_map_inline:cn would be the choice. – egreg Dec 09 '16 at 18:37
  • I just want to do a foreach loop basically, a bit like \dolistloop from etoolbox. Thanks I'll look into this – Marc Sharma Dec 09 '16 at 18:38
  • @MarcSharma Please, ask a followup question. – egreg Dec 09 '16 at 18:50
4

The following solution too is fast and defines one macro to hold the entire list. David Carlisle's scheme defines as many macros as there are list items. I don't know which one, between this one and David Carlisle's solution, requires less resources.

\documentclass{minimal}
\usepackage{etoolbox}
\makeatletter
\newcount\listcount
\def\list@list{}
\def\do#1{%
  \advance\listcount\@ne
  \edef\list@list{%
    \unexpanded\expandafter{\list@list}%
    \the\listcount{\unexpanded{#1}}%
  }%
}
\forcsvlist\do{%
  first element,second element,third element,fourth element,fifth element
}
% \getelement{<number>}
\def\getelement#1{%
  \def\reserved@a##1#1##2##3\listmark{%
    \edef\reserved@a{\unexpanded{##2}}%
    \ifx\reserved@a\@nnil
      \@latexerr{No item number '#1'}\@ehd
    \else
      ##2%
    \fi
  }%
  \expandafter\reserved@a\list@list#1{\@nil}\listmark
}
\makeatother
\begin{document}
\begin{itemize}
\item The third element is: ``\getelement{3}''.
\item The fourth element is: ``\getelement{4}''.
\item The fourth element again: ``\getelement{4}''.
\item The fifth element is: ``\getelement{5}''.
% This gives error:
%\item The non-existent element is: ``\getelement{100}''.
\end{itemize}
\end{document}

If you were to prettify your list as, e.g.,

\forcsvlist\do{%
  first element  ,
  second element ,
  third element  ,
  fourth element ,
  fifth element
}

the spaces before the list items are rightly removed by \forcsvlist (via \@iden), but not the spaces after the list items. In that case, list normalization is required.

Even without prettifying the list, the space after fifth element is retained. Look at your output. To avoid the trailing space, add the comment sign at the end of the list: fifth element%.

EDIT

Here is a solution that normalizes the list and is expandable. I still prefer this iterative solution to defining as many commands as the number of list items.

\documentclass{minimal}
\usepackage{catoptions}
\makeatletter
\newcount\gnilistcount
% \addlistitems{<listcmd>}{<items>}
\def\addlistitems#1#2{%
  \ifdefTF#1{}{\def#1{}}%
  \cptfor#2\dofor{%
    \advance\gnilistcount\@ne
    \edef#1{%
      \unexpanded\expandafter{#1}%
      \the\gnilistcount{\unexpanded{##1}}%
    }%
  }%
}
% \getelement{<number>}{<listcmd>}
\def\getelement#1#2{%
  \expandafter\gni@getelement#2\@nil\@nil\listmark{#1}%
}
\def\gni@getelement#1#2#3\listmark#4{%
  \ifstrcmpTF{#1}\@nil{%
    \@latexerr{No item number '#4'}\@ehd
  }{%
    \ifnumcmpTF#1=#4{%
      #2%
    }{%
      \gni@getelement#3\listmark{#4}%
    }%
  }%
}
 \makeatother

% Examples:
\addlistitems\gnilist{%
  first element  ,
  second element ,
  third element  ,
  fourth element ,
  fifth element
}
% Get third element in an \edef:
\edef\x{\getelement{3}\gnilist}
%\show\x

\begin{document}
\begin{itemize}
\item The third element is: ``\getelement{3}\gnilist''.
\item The fourth element is: ``\getelement{4}\gnilist''.
\item The fourth element again: ``\getelement{4}\gnilist''.
\item The fifth element is: ``\getelement{5}\gnilist''.
% This gives error:
%\item The non-existent element is: ``\getelement{100}\gnilist''.
\end{itemize}
\end{document}

Here is a more general \addlistitems that also normalizes the list before saving it.

\documentclass{minimal}
\usepackage{catoptions}
\makeatletter
% \addlistitems[<optional parser>]{<listcmd>}{<items>}
% \addlistitems*[<optional parser>]{<listcmd>}{<itemcmd>}
% The same item may be entered more than once, but with different
% serial numbers. If this isn't the desired spec, then the OP should
% say so.
\robust@def*\addlistitems{\cpt@teststopt\gni@addlistitems,}
\robust@def*\gni@addlistitems[#1]#2#3{%
  \begingroup
  \ifdefTF#2{}{\def#2{}}%
  \edef\tempb{\cptremovescape#2}%
  \ifcsndefTF{listcount@\tempb}{}{%
    \csn@xdef{listcount@\tempb}{0}%
  }%
  \def\csv@do##1{%
    \aftercsname\cptpushnumber{listcount@\tempb}%
    \edef#2{%
      \unexpanded\expandafter{#2}%
      \usecsn{listcount@\tempb}{\unexpanded{##1}}%
    }%
  }%
  \edef\tempa{\csv@@parse\ifcpt@st*\fi}%
  \tempa[#1]{#3}%
  \postgroupdef#2\endgroup
}
% \getelementofnumber{<number>}{<listcmd>}
% This can be used for a general list command <listcmd>.
\new@def*\getelementofnumber#1#2{%
  \expandafter\gni@getelement#2\@nil\@nil\listmark{#1}%
}
\new@def*\gni@getelement#1#2#3\listmark#4{%
  \ifstrcmpTF{#1}\@nil{%
    \@latexerr{No item number '#4'}\@ehd
  }{%
    \ifnumcmpTF#1=#4{%
      #2%
    }{%
      \gni@getelement#3\listmark{#4}%
    }%
  }%
}
\makeatother

% Example:
\addlistitems\gnilist{%
  element 1 ,
  element 2 ,
  element 3 ,
  element 4 ,
  element 5
}
% The list separator is changed here:
\addlistitems[;]\gnilist{%
  element 6  ;
  element 7  ;
  element 8  ;
  element 9  ;
  element 10 ;
  % If you don't want 'element 10' entered twice, say so:
  element 10 ;
}
%\show\gnilist

% Get element no. 3 in an \edef:
\edef\x{\getelementofnumber{3}\gnilist}
%\show\x

\def\getel#1{\getelementofnumber{#1}\gnilist}

\begin{document}
\begin{itemize}
\item The third element is: ``\getel{3}''.
\item The fourth element is: ``\getel{4}''.
\item The fourth element again: ``\getel{4}''.
\item The fifth element is: ``\getel{5}''.
\item The ninth element is: ``\getel{9}''.
% This gives error:
%\item The non-existent element is: ``\getel{100}''.
\end{itemize}
\end{document}
Ahmed Musa
  • 11,742
  • Thanks for your answer. I actually don't understand everything in this code. That'll give me a little bit of study to do! BUT there's a slight problem (hope you'll be able to explain why this happens): tried to \edef\fourthelement{\getelement{4}} and this yields the same error as in the OP... Any clue? – gniourf_gniourf Nov 03 '12 at 22:46
  • OK, I didn't realize that you needed an expandable solution. That will certainly require more resources, either David Carlisle's store-based approach or an iterative one. Mine will have to be an iterative one, which will be slower than David's solution. – Ahmed Musa Nov 03 '12 at 22:51
  • Thanks for your edit, it's beautiful! I believe it will be a good resource for other people too! Is it possible to do the same without the package catoptions? – gniourf_gniourf Nov 04 '12 at 10:15
  • Is it possible to do the same without the catoptions package? Of course, but not by me now. – Ahmed Musa Nov 04 '12 at 16:16
1

With functional package, you can write the following code:

\documentclass{article}

\usepackage{functional}

\SeqNew \lMySeq \SeqSetFromClist \lMySeq { first element, second element, third element, fourth element, fifth element }

\PrgNewFunction \getnthelement {m} {% \SeqVarItem \lMySeq {#1}% }

\PrgNewFunction \storenthelement {Mm} {% \TlSet #1 {\SeqVarItem \lMySeq {#2}}% }

\storenthelement \fourthelement {4}

\begin{document}

\begin{itemize} \item The third element is: \getnthelement{3}''. \item The fourth element is:\fourthelement''. \item The fourth element again: \fourthelement''. \item The fifth element is:\getnthelement{5}''. \end{itemize}

\end{document}

enter image description here

L.J.R.
  • 10,932
0

I know, the original question is quite old, but maybe someone's looking for a straight forward solution using a loop (from pgffor package, which is included in tikz) and a counter:

\documentclass{standalone}

\usepackage{pgffor}

\newcounter{itemcount} \newcommand{\getitem}[2]{% \setcounter{itemcount}{0}% \foreach \i in #1 {% \ifnum\theitemcount=#2{\i}{}\fi% \stepcounter{itemcount}% }% }

\def\mylist{a,b,c,d,e}

\begin{document}

\getitem{\mylist}{0} \getitem{\mylist}{2} \getitem{\mylist}{4} \getitem{\mylist}{1} \getitem{\mylist}{3}

\end{document}

Output:

enter image description here

0

A list in can be created with etoolbox package using \listadd. By defining a custom function \getValueAtIndex an item from the previously generated list can be extracted.

\documentclass[]{report}
\usepackage{etoolbox}

% Define a list with 5 items first \listadd\localList{} \listadd{\localList}{8} \listadd{\localList}{4} \listadd{\localList}{3} \listadd{\localList}{6} \listadd{\localList}{34}

\newcounter{itemcount} \newcommand{\getValueAtIndex}[2]{ % 1. Argument: index % 2. Argument: list \setcounter{itemcount}{0}

\def\indexToRetrieve{#1}
\renewcommand*{\do}[1]{%
    \ifnum\number\value{itemcount}=\indexToRetrieve
    {##1}\else{}\fi
    \stepcounter{itemcount}}
\dolistloop{#2}

}

\begin{document} Second element: \getValueAtIndex{1}{\localList}\ Fifth element: \getValueAtIndex{2}{\localList} \end{document}

Which results in: enter image description here

Improvement:

\newcounter{itemcount}
\newcommand{\getValueAtIndex}[2]{%
    \setcounter{itemcount}{0}%
    \def\indexToRetrieve{#1}%
    \renewcommand*{\do}[1]{%
        \ifnum\number\value{itemcount}=\indexToRetrieve{##1}\else{}   \fi\stepcounter{itemcount}}%
    \dolistloop{#2}%
}
paoloberm
  • 1
  • 1
0

This is what the listofitems package is made for. Once read, the list items are fully expandable (requiring two expansions to recover the original tokens). The package supports nested list parsing as well, to essentially create multi-dimensional lists.

\documentclass{article}
\usepackage{listofitems}
\begin{document}
\def\mylistdata{%
   first element,
   second element,
   third element,
   fourth element,
   fifth element
}%
\readlist*\mylist{\mylistdata}
\begin{itemize}
\item The list has a total of \listlen\mylist[] items.
\item The third element is: ``\mylist[3]''.
\item The fourth element is: ``\mylist[4]''.
\item The fourth element again: ``\mylist[4]''.
\item The fifth element is: ``\mylist[5]''.
\end{itemize}
\end{document}

enter image description here