10

I'm looking for a way to realize a macro with a switch/case control structure (or an array solution), as it exists in every programming language.

MWE:

\documentclass[ngerman,12pt]{article}
\usepackage{etoolbox}

\newcommand*\foo[1]{%
    \ifstrequal{#1}{givemea}{box}{}%
    \ifstrequal{#1}{drawmea}{circle}{}%
    \ifstrequal{#1}{applesidontlikeare}{green}{}%
    \ifstrequal{#1}{life}{is full of surprises}{}%
}
\begin{document}
\foo{life}
\end{document}

As you can think of, if I have let's say 500 entries the process will slow down very much, as it compares to every single value in the macro. A \break or \return command would be very useful, so the macro aborts if the value is found. Addition: My strings also will contain numbers (at every position).

Or are there any packages I can use for the job?

(Since there will be a lot of entries, I consider to split up the one macro into many: \fooa, \foob, ... So, if I invoke \foo{life} it checks the first letter l, and then invokes \fool{life}. That also will speed up the process.)

musicman
  • 3,559
  • Comparing every item is going to be slow: a split if possible is useful, even if it's only say halving the total length. – Joseph Wright May 28 '14 at 09:04
  • 2
    Another alternative is an "associative jump array" using \csname foo@branch@#1\endcsname where the command \foo@branch@gievemea contains the code for this value. This will use the internal hash which is really efficient. You can guard this with \ifcsname and make it more robust with \detokenize. – Stephan Lehmke May 28 '14 at 09:09
  • @StephanLehmke I agree, but my strings will also have numbers, see my comment to David Carlisles answer – musicman May 28 '14 at 09:27
  • 1
    @musicman No problem, just define the commands with \expandafter\newcommand\csname foo@branch@abc123(bar);baz\endcsname{expansion text} – Stephan Lehmke May 28 '14 at 09:29
  • 1
    As for splitting up, using the internal hash will be efficient for millions of entries; see this question. – Stephan Lehmke May 28 '14 at 09:33
  • Using the \@namedef trick is certainly the way to go here but depending on the application you have in mind you might also find the package pgfkeys useful. Adopting a key-val interface could make your code very readable and flexible, while avoiding reinventing the wheel – Bordaigorl May 28 '14 at 11:13

3 Answers3

14

You don't want a linear series of string equality tests you want a hash lookup:

\documentclass[12pt]{article}


\newcommand*\foo[1]{\csname FOO-#1\endcsname}
\newcommand*\setfoo[1]{\expandafter\def\csname FOO-#1\endcsname}

\setfoo{give1mea}{box}
\setfoo{draw2mea}{circle}
\setfoo{applesidontlikeare!!}{green}
\setfoo{life}{is full of surprises}
\setfoo{apple4dinner}{see?}


\begin{document}
\foo{life} \foo{apple4dinner}
\end{document}
David Carlisle
  • 757,742
  • Sorry, I can't use that, because I also will have number in the arguments: apple4dinner, 4bears, goodn8 – musicman May 28 '14 at 09:24
  • @musicman That isn't a problem just use \@namedef{apple4dinner}{this} (somewhere were @ is a letter eg after \makeatletter or in a package – David Carlisle May 28 '14 at 09:26
  • @musicman updated answer to demonstrate digits and ! in mames – David Carlisle May 28 '14 at 09:33
  • If \foo is not \long, shouldn't \setfoo also not be \long? – Andrew Stacey May 28 '14 at 09:34
  • @AndrewStacey not really one is saying whether you want to allow \par in the name the other whether you want to allow \par in the value. You may as well make \foo not long as \par would generate an error in csname anyway, but you may want \par in the value – David Carlisle May 28 '14 at 09:48
  • @AndrewStacey A \par in the argument to \setfoo would give an error anyway (Missing \endcsname). – egreg May 28 '14 at 09:48
  • @DavidCarlisle You misunderstand where I want to put the \long. Since you define \foo using \newcommand* then I think you should also define \setfoo using \newcommand* as the arguments are used in the same way. Whether or not you want to allow \foo and \setfoo to eat a \par is, as both you and egreg point out, simply a matter of choosing your error message. But whatever you choose, you should be consistent about your choice. – Andrew Stacey May 28 '14 at 09:52
  • @AndrewStacey oh I was thinking I'd defined \setfoo to have two arguments (in which case my comment above would be true) but I see I took a shortcut and only defined it with 1. Now I have a moral dilemma of whether to comment that you were correct or amend the answer so my comment is correct – David Carlisle May 28 '14 at 09:56
  • 1
    @AndrewStacey I added a * :-) – David Carlisle May 28 '14 at 09:57
  • @DavidCarlisle I applaud your sense of morality. – Andrew Stacey May 28 '14 at 10:07
  • I've decided to accept your answer, because it's the most 'basic', it doesn't need any package etc. - thanks for all your answers! – musicman May 28 '14 at 12:17
10

The expl3 programming layer for LaTeX3 provides \str_case:nnF and related functions: these only compare 'as far as required':

\documentclass{article}
\usepackage{expl3}
\ExplSyntaxOn
\cs_new_eq:NN \strcases \str_case:nnF
\ExplSyntaxOff
\newcommand*\foo[1]{%
  \strcases{#1}%
    {%
      {givemea}           {box}
      {drawmea}           {circle}
      {applesidontlikeare}{green}
      {life}              {is full of surprises}
    }%
    {No match found!}%
}
\begin{document}
\foo{life}
\end{document}

Note that for very long lists the comparison still has to be made for each item until a hit is found, which will get slower for very long lists when the item is near the end.

An alternative approach, similar to David's in the sense it uses a lookup table, is to use a prop:

\documentclass{article}
\usepackage{expl3}
\ExplSyntaxOn
\prop_new:N \l_my_prop
\prop_put:Nnn \l_my_prop {givemea}           {box}
\prop_put:Nnn \l_my_prop {drawmea}           {circle}
\prop_put:Nnn \l_my_prop {applesidontlikeare}{green}
\prop_put:Nnn \l_my_prop {life}              {is~full~of~surprises}
\newcommand* \foo [1]
  {
    \prop_get:NnNTF \l_my_prop {#1} \l_tmpa_tl
      { \tl_use:N \l_tmpa_tl }
      { No~match~found! }
}
\ExplSyntaxOff
\begin{document}
\foo{life}
\end{document}

The data structure here (currently) uses only one name but that means that addition is relatively slow as the size rises. However, lookup should be not so severely affected (it uses a delimited macro), though again this drops off as the table size rises. (For 'big' data sets, TeX's hash table is probably the best way to create a lookup system, as in David's answer. See How to implement (low-level) arrays in TeX for more.)

Joseph Wright
  • 259,911
  • 34
  • 706
  • 1,036
  • Wouldn't a prop be more efficient here? – Andrew Stacey May 28 '14 at 09:33
  • @AndrewStacey I've added a second approach: note that as we currently use on macro for the storage, things do slow down for big data sets. The hash table approach is probably better in those situations: mappings on the other hand are easier with a single macro. – Joseph Wright May 28 '14 at 10:23
  • What do you think: Which method – your 2 approaches and David Carlisles – comes with the best performance? – musicman May 28 '14 at 10:58
  • 1
    @musicman As indicated in the link I've edited in, hash table based approaches have the performance advantage. However, they are not so easy to do certain tasks with (e.g. copy the whole table): the best choice depends on your real use case. – Joseph Wright May 28 '14 at 11:49
7

The following example compares the strings until a match if found. Then \@car takes the first argument and throws the remaining part until and including the next \@nil away, so that the following comparisons are skipped:

\documentclass[12pt]{article}
\usepackage{etoolbox}

\makeatletter
\newcommand*\foo[1]{%
  \def\foo@##1##2{%
    \ifstrequal{#1}{##1}{\@car{##2}}{}%
  }%
  \foo@{givemea}{box}%
  \foo@{drawmea}{circle}%
  \foo@{applesidontlikeare}{green}%
  \foo@{life}{is full of surprises}%
  \@nil
}
\makeatother
\begin{document}
\foo{givemea} \foo{life}
\end{document}

Result

Heiko Oberdiek
  • 271,626