6

I have the following problem: I am using the etoolbox package to check if a macro (say, \foo) expands to one of several possible strings (say, bar, baz, abc, or other). I need to execute different code depending on what string \foo expands to.

My current solution is

\ifdefstring{\foo}{bar}
{% if \foo is bar
  <do something>
}
{% if \foo is not bar
  \ifdefstring{\foo}{baz}
  {% if \foo is baz
    <do something else>
  }
  {% if \foo is not bar or baz
    \ifdefstring{\foo}{abc}
    {% if \foo is abc
      <do something else>
    }
    {% if \foo is not bar, baz, or abc
      <do something else>
    }
  }
}

Although this works, it is not satisfactory due to the deep nesting, and it doesn't generalize well to longer lists of possible strings. In my real situation, I have up to eight possible strings to check, so this solution creates unreadable code.

My question is, what alternative options (in LaTeX2e) do I have to check if a macro expands to one of several strings?

Ideally, the solution would also work for other etoolbox comparison commands (e.g., \ifstrequal). However, the solution need not use the etoolbox package.

4 Answers4

7

If \foo expands to a simple string, which survives a \csname, the following method can be used, which does not need LaTeX3:

\documentclass{article}

\makeatletter
\@namedef{foo@bar}{%
  \typeout{\noexpand\foo is bar}%
  % <do something>
}
\@namedef{foo@baz}{%
  \typeout{\noexpand\foo is baz}%
  % <do something>
}
\@namedef{foo@abc}{%
  \typeout{\noexpand\foo is abc}%
  % <do someting>
}

\newcommand*{\evaluate@foo}{%
  \@ifundefined{foo@\foo}{%
    \errmessage{\noexpand\foo has unknown value}%
  }{%
    \@nameuse{foo@\foo}%
  }%
}
\providecommand*{\foo}{}% initialization

% Test

\def\foo{baz}
\evaluate@foo

\def\foo{abc}
\evaluate@foo

\def\foo{xyz}
\evaluate@foo

\makeatletter

\begin{document}
\end{document}

Result:

\foo is baz
\foo is abc
! \foo has unknown value.
Heiko Oberdiek
  • 271,626
4

The best solution I can think of is expl3’s \str_case:nnTF. Here I generate a variant to take one token instead of a test string and compares to expansion of that token to the list. If no match is found in the list an appropriate message is output.

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn

\cs_generate_variant:Nn \str_case:nnF { VnF }

\cs_new_protected:Npn \artem_testfoo:N #1
 {
  \texttt{ \cs_to_str:N #1 } ~ 
  \str_case:VnF #1
   {
    { bar } { is ~ bar }
    { baz } { is ~ baz }
    { abc } { is ~ abc }
   }
   { is ~ neither ~ bar, ~ nor ~ baz, ~ nor ~ abc. }
 }

\NewDocumentCommand \testfoo { m }
 {
  \artem_testfoo:N #1
 }

\ExplSyntaxOff
\begin{document}

\def\foo{bar}
\testfoo\foo

\def\foo{baz}
\testfoo\foo

\def\foo{abc}
\testfoo\foo

\def\foo{xyz}
\testfoo\foo

\end{document}

enter image description here

Henri Menke
  • 109,596
3

It's quite easy with xparse and expl3:

\documentclass{article}

\usepackage{xparse}
\ExplSyntaxOn
\DeclareExpandableDocumentCommand{\xifstring}{mO{}m}
 {
  \str_case:onF { #1 } { #3 } { #2 }
 }
\ExplSyntaxOff

\begin{document}

\newcommand{\foo}{abc}

\xifstring{\foo}[No match]
 {
  {abc}{String is `abc'}
  {def}{String is `def'}
  {ghi}{String is `ghi'}
 }

\renewcommand{\foo}{def}

\xifstring{\foo}[No match]
 {
  {abc}{String is `abc'}
  {def}{String is `def'}
  {ghi}{String is `ghi'}
 }

\renewcommand{\foo}{ghi}

\xifstring{\foo}[No match]
 {
  {abc}{String is `abc'}
  {def}{String is `def'}
  {ghi}{String is `ghi'}
 }

\renewcommand{\foo}{foo}

\xifstring{\foo}[No match]
 {
  {abc}{String is `abc'}
  {def}{String is `def'}
  {ghi}{String is `ghi'}
 }

\end{document}

The optional argument is for what you want when there's no match (default value, do nothing).

enter image description here

egreg
  • 1,121,712
  • Thank you for taking time to answer! Unfortunately, I'm not familiar with LaTeX3 at all (I should have specified that in the question). – Artem Mavrin Sep 10 '15 at 23:46
  • Why do you need to be familiar with latex3? Just think of it as a package that you load that provides programming tools. – Will Robertson Sep 11 '15 at 00:58
3

The simple minded method from my other answer avoids nesting. Here we don't have TeX like \if..\fi but suitable wrappers, so we need to modify a bit the definitions.

\long\def\DoThis #1#2\OrThat #3{#1}
\long\def\OrThat #1{#1}% \@firstofone

\ifdefstring{\foo}{bar}{\DoThis{do something}}{}%
\ifdefstring{\foo}{baz}{\DoThis{do something else}}{}%
\ifdefstring{\foo}{bat}{\DoThis{do something different}}{}%
\OrThat {in case all tests fail}%

Remark: once a test is positive all successive ones are skipped, due to the definition of \DoThis macro.

  • the method applies to all possible branching conditional of the type \ifsometest{..}...{YES}{NO} . –  Sep 11 '15 at 11:30