# Store several values in an array-like way and then access them

I am building a kind of dictionary that consists of two parts: in the first one there are words, and in the second, roots. I would like to automatically keep track of which words are related to which roots. I already have the information to do so:

% \entryword[ROOT]{LANGUAGE}{WORD}{DESCRIPTION}
% \entryword[serve]{english}{reserve}{This is a wonderful word.}
% \entryword{english}{thing}{There's no root for this one.}
\newcommand{\entryword}[4][]{
\ifthenelse{\isempty{#1}}
{}
{
% The optional argument, if not empty, represents the root.
% Now, I would like to annotate this word in the list of
% relatives of the specified root. It should be done here, I think.
}
\entryprompt{\foreignlanguage{#2}{#3}}
#4

\ifthenelse{\isempty{#1}}
{}
{
Root: \textit{-#1-}.
}
}


By having the relatives of each root conveniently stored in an array-like manner, I could then automatically print them:

% \entryroot{serve}{This root comes from latin.}
\newcommand{\entryroot}[2]{
\entryprompt{\textit{-#1-}}
#2

% Here, how could I automate it?
Relatives: .
}


A MWE:

\documentclass[b5paper,11pt]{report}

\usepackage[b5paper,margin=1.5cm]{geometry}
\usepackage{multicol}
\usepackage[main=english]{babel}
\usepackage[utf8]{inputenc}
\usepackage{xifthen}

\newcommand{\entryprompt}[1]{
{
\par\noindent
\textbf{\textsf{#1}}
}
}

\newcommand{\entryword}[4][]{
\ifthenelse{\isempty{#1}}
{}
{
% The optional argument, if not empty, represents the root.
% Now, I would like to annotate this word in the list of
% relatives of the specified root.
}
\entryprompt{\foreignlanguage{#2}{#3}}
#4

\ifthenelse{\isempty{#1}}
{}
{
Root: \textit{-#1-}.
}
}

\newcommand{\entryroot}[2]{
\entryprompt{\textit{-#1-}}
#2

% Here, how could I automate it?
Relatives: .
}

\begin{document}
\thispagestyle{empty}
\begin{center}
\Large
\textbf{Words}
\end{center}

\newpage

\begin{multicols}{2}
\entryword[serve]{english}{reserve}{This is a wonderful word.}
\entryword{english}{thing}{There's no root for this word.}
\end{multicols}

\newpage

\thispagestyle{empty}
\begin{center}
\Large
\textbf{Roots}
\end{center}

\newpage

\begin{multicols}{2}
\entryroot{load}{I don't know where this one comes from.}
\entryroot{serve}{This root comes from latin.}
\end{multicols}
\end{document}


Ideally, the relatives would be stored in a kind of table that could then be traversed, so that they could be printed in several ways. I've already looked at the arrayjobx package, and it doesn't seem to fill my requirements. In particular:

• Strings can't be indices.
• It doesn't seem to offer a simple way to add an element at the end of an array.

As egreg suggested, I'm adding a sample of the desired output:

-- Words --

reserve - This is a wonderful word. Root: serve.

thing - There's no root for this word.

-- Roots --

load - I don't know where this one comes from.

serve - This root comes from latin. Relatives: reserve.

• An example of what output you're expecting would be useful. – egreg Mar 8 '15 at 14:59
• The current example also works with LuaTeX – percusse Mar 13 '15 at 20:53
• @percusse I meant, a solution implemented in Lua. I did not understand the solution provided by egreg; that was probably the reason I could not hack it as I wanted. Therefore, I tried to go for LuaTeX, but I don't get it yet. – Kalrish Mar 13 '15 at 21:02

You can define an array that's loaded with the relatives to a root, when \entryword is processed. When you use \entryroot, the array is printed.

\documentclass{article}
\usepackage[english]{babel}
\usepackage{xparse}

\NewDocumentCommand{\entryprompt}{mm}{%
\par\noindent\textbf{\textsf{#1}} #2%
}

\ExplSyntaxOn
\NewDocumentCommand{\entryword}{ommm}
{
\entryprompt{\foreignlanguage{#2}{#3}}{#4}
\IfValueT{#1}
{% if a root is present, print it
\ Root:\nobreakspace#1
% then add the word as a relative to the root
\kalrish_addtoroot:nn { #3 } { #1 }
}
}
\NewDocumentCommand{\entryroot}{mm}
{
\par\noindent
#1\nobreakspace--\nobreakspace#2
% if the root has relatives, the corresponding sequence exists
\seq_if_exist:cT { g_kalrish_root_ \tl_to_str:n { #1 } _seq }
{
% start printing the relatives
\c_space_tl Relatives:~
% separated by a comma
\seq_use:cn { g_kalrish_root_ \tl_to_str:n { #1 } _seq } { ,~ }
}
}

{
% if a root appears for the first time, allocate a sequence
\seq_if_exist:cF { g_kalrish_root_ \tl_to_str:n { #2 } _seq }
{
\seq_new:c { g_kalrish_root_ \tl_to_str:n { #2 } _seq }
}
% add the current word as a relative
\seq_gput_right:cn { g_kalrish_root_ \tl_to_str:n { #2 } _seq } { #1 }
}

\ExplSyntaxOff

\begin{document}

\section{Words}

\entryword[serve]{english}{reserve}{This is a wonderful word.}

\entryword{english}{thing}{There's no root for this word.}

\entryword[serve]{english}{service}{This is also a word of tennis}

\section{Roots}

\entryroot{load}{I don't know where this one comes from.}

\entryroot{serve}{This root comes from latin.}

\end{document}


• It works like a charm. Fantastic! – Kalrish Mar 8 '15 at 16:00
• I have been trying it, but couldn't get it: how would one make each of the relatives appear as, say, italic? Where would one insert the textit{…}? – Kalrish Mar 13 '15 at 23:35
• @Kalrish Say \textit{\seq_use:cn ...} in the line after the “Separated by commas” comment – egreg Mar 13 '15 at 23:44
• That way, either the comma itself is italicized (which I don't like) or bad output is produced. As a more compelling task, what if one wanted to make a cross-reference to the place where the word is defined? (One would need the word itself) – Kalrish Mar 14 '15 at 14:10
• @Kalrish I can't guess what you have in mind. A new question with a complete set of specifications will surely help better. – egreg Mar 14 '15 at 14:19

Another solution without the need of any package (because only TeX primitives and basic macros are used):

\def\sxdef#1{\expandafter\xdef\csname#1\endcsname}
\def\entryword{\def\opt{}\futurelet\next\entrywordA}
\def\entrywordA{\ifx\next[\expandafter\entrywordB \else \expandafter\entrywordC \fi}
\def\entrywordB[#1]{\def\opt{#1}\entrywordC}
\def\entrywordC#1#2#3{{\bf#2} #3%
\ifx\opt\empty \else \space Root: \opt
\expandafter\ifx \csname r:\opt\endcsname \relax \sxdef{r:\opt}{#2}%
\else \sxdef{r:\opt}{\csname r:\opt\endcsname, #2}\fi
\fi}
\def\entryroot#1#2{#1 -- #2%
\expandafter \ifx \csname r:#1\endcsname \relax \else
\space Relatives: \csname r:#1\endcsname \fi
}

\entryword[serve]{english}{reserve}{This is a wonderful word.}

\entryword{english}{thing}{There's no root for this word.}

\entryword[serve]{english}{service}{This is also a word of tennis}

\entryroot{load}{I don't know where this one comes from.}

\entryroot{serve}{This root comes from latin.}

\end


This is a demonstration suitable for further editing, the skeleton is programmed in LuaLaTeX.

We store all the words and roots in two external files and load them on next TeX runs. The key line is \newcommand\entryword[4][]{\directlua{storeword([[#1]],[[#2]],[[#3]],[[#4]])}} which passes all TeX arguments to Lua, into the storeword function in this example. We could even process TeX file (plain text) by Lua without TeXing.

There are many possible improvements, we could add:

• Page numbers: definitions<->lists in both directions (\label, \ref, \pageref).
• Cross references: words<->roots in both directions (with or without testing of the presence of the entries).
• Sort the entries (e.g. in Lua, MakeIndex, Xindy, Biber, ...). Now, the entries are hashed, so after each run of TeX we may get different order of entries. Try it for yourself. :-)

We run lualatex, I enclose the mal-root.tex file and a preview of the result.

% lualatex mal-root.tex
\documentclass[a4paper]{article}
\pagestyle{empty}
\usepackage{luacode}
\parindent=0pt

\begin{document}
\begin{luacode*}
malwords={}
malroots={}

-- Store a single word to a data table...
function storeword(root,lang,word,text)
-- print(root,lang,word,text)
-- split root(s) to a table
roots={}
for singleroot in string.gmatch(root, "([^,]+)") do
table.insert(roots, singleroot)
end
malwords[word]={ roots, lang, text }
end -- function storeme

-- Store a single root to a data table...
function storeroot(root,text)
-- print(root,text)
malroots[root]={ text }
end -- function storeroot

-- Print both lists (words, roots)...
function printall(orderfile)
-- load stored files if they exist
for _,malf in pairs({"dumpwords.lua","dumproots.lua"}) do
f=io.open(malf,"r")
if f then f:close(); dofile(malf); end -- print(malf);
end -- for, files

-- Typeset the first list...
if orderfile==1 and malwordsfull then  -- words
tex.print("\\section{List of Words}")
for theword,term in pairs(malwordsfull) do
mals="\\textbf{"..theword.."}  -- "..term[3]
if #term[1]>0 then
mals=mals.." Root"
if #term[1]>1 then mals=mals.."s" end
mals=mals..": "
for elemi,elem in pairs(term[1]) do
mals=mals..elem
if elemi<#term[1] then mals=mals..", " end
end -- for
end -- if #>0
mals=mals.."\\par"
tex.print(mals)
end
end -- words

-- Typeset the other list (to add cross references?)

if  orderfile==2 and malrootsfull then -- roots
tex.print("\\section{List of Roots}")

-- Check all roots...
for theword,term in pairs(malrootsfull) do
mals="\\textbf{"..theword.."} -- "..term[1]
-- Check all words...
for elem,oword in pairs(malwordsfull) do
-- Check all roots in words...
for oroota,oroot in pairs(oword[1]) do
end -- for rootsfull
end -- if orderfile
if counter>0 then
mals=mals.." Relative"
if counter>1 then mals=mals.."s" end
end -- if
mals=mals.."\\par"
tex.print(mals)
end -- for

end -- if
end -- function printwords()

-- http://stackoverflow.com/questions/9168058/lua-beginner-table-dump-to-console
function dumper(o)
if type(o) == 'table' then
local s = '\n{'
for k,v in pairs(o) do
if type(k) ~= 'number' then k = '"'..k..'"' end
s = s .. '['..k..'] = ' .. dumper(v) .. ', '
end
return s..'}\n'
else
return '"'..tostring(o)..'"'
end
end

-- Save data to a single file...
function savedata(file, data, name)
saveme=io.open(file, "w")
mojkovo=name.." = "..dumper(data, name)
saveme:write(mojkovo)
saveme:close()
end

-- Save both data tables...
function saveall()
savedata("dumpwords.lua", malwords, "malwordsfull")
savedata("dumproots.lua", malroots, "malrootsfull")
end
\end{luacode*}

% Let's call Lua...
\newcommand\entryword[4][]{\directlua{storeword([[#1]],[[#2]],[[#3]],[[#4]])}}
\newcommand\entryroot[2]{\directlua{storeroot([[#1]],[[#2]])}}
\newcommand\printwords{\directlua{printall(1)}}
\newcommand\printroots{\directlua{printall(2)}}
\newcommand\savedata{\directlua{saveall()}}

% Call Lua to work (data from the previous TeX run)...
\printwords
\printroots

% Let's test if it works (some words) and ...
\entryword[sleep1,sleep2]{english}{sleeping}{A word with more roots.}
\entryword[serve]{english}{reserve}{This is a wonderful word.}
\entryword{english}{thing}{There's no root for this word.}
\entryword[serve]{english}{service}{This is also a word of tennis.}

% ... and (some roots)
\entryroot{load}{I don't know where this one comes from.}
\entryroot{serve}{This root comes from latin.}

% Save all data to two external files for next TeX run...
\savedata
\end{document}


• I'm keeping egreg's answer as my accepted answer because he first answered what I asked. Nevertheless, this is a fantastic answer. It seems I am still hitting a bug with some packages, unfortunately. – Kalrish Mar 17 '15 at 22:46
• @Kalrish If you get stuck, please feel free to modify the original question or post a new one. We are all bug hunters and hungry enough to get them... :-) – Malipivo Mar 18 '15 at 6:31