6

Consider the following code

\documentclass{article}
\usepackage{xifthen}

\def\question#1 #2 #3?{\ifx#1 #2 #3?\relax\else#1 #2 #3 or\expandafter\question\fi}
\newcommand{\test}[1]{\question#1}
\begin{document}
\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?}
\end{document}

gives the output as

Ab Bc Cd or Tm Br Sl or Kr Nd Zf or

and an error: "Paragraph ended before \question was complete."

How does one improve this such that the final "or" does not print?

Goal: "Ab Bc Cd or Tm Br Sl or Kr Nd Zf" and so on for more complicated pattern matching.

How does one improve this code to correct the error?

A related question:

\def\splitter#1 #2 #3?#4{#1 #2 #3\ifthenelse{\isempty{#4}}{}{ \(|\) \splitter#4}}

works even worse, with the last three terms going missing and a line break appearing, plus an error? Why?

I tried reading one of the TeX related books on recursive macros but they propose getting a count done on the arguments, or a form similar to the second case, which inserting relax in places does not seem to make work.

2
  • 1
    You don't say what you want this code to do. \ifx#1 is comparing A to b it's never true if your first letter is upper case and your second is lower case. Feb 23 at 17:27
  • Ty for all the solutions. I really like all the answers in that they help understand the original type of error, which is more broad than the special case here, and each gives different approach that teaches a different thing about LaTex. All four answers are useful and in different contexts, one can presume the different approaches would have different advantages: some shortness, others generality, some break others not. Will mark one as answer shortly. Feb 24 at 3:26

6 Answers 6

7

It helps having a terminator to the data in this case \endq, because it eliminates the need to look ahead (perhaps with \futurelet) to find the end of the data stream.

Also, a terminator in the definition of \test eliminates ambiguities, such as would arise if you executed the following:

\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?} Am I done?

The MWE:

\documentclass{article}
\newif\ifneedor
\def\question#1 #2 #3? #4\endq{%
  \ifx\relax#4\relax
    \def\next{}%
  \else
    \def\next{\question#4\endq}%
  \fi
  \ifneedor\ or \else\needortrue\fi
  #1 #2 #3\next}
\newcommand{\test}[1]{\needorfalse\question#1 \endq}
\begin{document}
\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?}.
\end{document}

enter image description here

Here's an alternate form that eliminates \ifneedor:

\documentclass{article}
\def\question#1 #2 #3? #4\endq{%
  #1 #2 #3%
  \ifx\relax#4\relax
    \def\next{}%
  \else
    \ or \def\next{\question#4\endq}%
  \fi
  \next}
\newcommand{\test}[1]{\question#1 \endq}
\begin{document}
\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?}.
\end{document}
6

I think your main mistake is

\ifx#1

It's not clear what you intended to compare but \ifx compares tokens so here it is comparing A to b: it's never true if your first letter is upper case and your second is lower case.

As far as I can tell you don't need the space delimited arguments you can simply iterate through ?. It would be easier if your input didn't have the final ? but this removes it in an extra step.

enter image description here

\documentclass{article}


\def\question#1?\xquestion{\xquestion#1\yquestion?}
\def\xquestion#1?{#1 or\xquestion}
\def\yquestion#1#2#3{}

\newcommand{\test}[1]{\question#1\xquestion}

\begin{document}
\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?}
\end{document}
1
  • Very nice end-condition. I was too preoccupied with building a nice loop, that I totally forgot that #1 will be fully evaluated...
    – Skillmon
    Feb 23 at 22:04
6

Sorry, but your code is wrong not only because of a missing termination.

Let's see what happens with your call. From

\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?}

we get

\question Ab Bc Cd? Tm Br Sl? Kr Nd Zf?

Now \question grabs delimited arguments and in this case you get

#1 <- Ab
#2 <- Bc
#3 <- Cd

and the input stream will become

\ifx Ab Bc Cd?\relax\else Ab Bc Cd or\expandafter\question\fi Tm Br Sl? Kr Nd Zf?

Now \ifx compares A with b and the false branch is taken.

But what if you had AA instead of Ab?

In this case, the false branch would be skipped and you'd get

 Bc Cd? Tm Br Sl? Kr Nd Zf?

with no recursion at all.

If your aim is to replace ? with “or” just in the middle, you need a completely different approach.

The simplest one:

\documentclass{article}

\ExplSyntaxOn

\NewDocumentCommand{\test}{m}{%
  \seq_set_split:Nnn \l_tmpa_seq { ? } { #1 }
  % check for a trailing ? which is present if the last item is empty
  \seq_pop_right:NN \l_tmpa_seq \l_tmpa_tl
  \tl_if_empty:NF \l_tmpa_tl
   {% no trailing ?, restore the item
    \seq_put_right:NV \l_tmpa_seq \l_tmpa_tl
   }
  \seq_use:Nn \l_tmpa_seq { ~or~ }
 }

\ExplSyntaxOff

\begin{document}

\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?}

\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf}

\end{document}

enter image description here

1
  • @Skillmon Yes, I had fixed in my file and forgot to fix the code here.
    – egreg
    Feb 24 at 9:27
5

Your terminal condition is never reached (you're comparing #1 to a space if #1 is a single token, #1 is never a space, or the first two tokens in #1 if there is more than one), hence you get the error and also the additional or. The only reason the recursion stopped is because of the error.

You could do this in the following way instead:

\documentclass{article}

\makeatletter
\@ifdefinable\question
  {\def\question#1?{\question@check#1\question@done#1 or\question}}
\def\question@check#1\question@done{}
\def\question@done#1\question{}

\newcommand*\test[1]{\question#1\question@done?}
\makeatother

\begin{document}
\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?}
\end{document}

This works by placing an explicit end marker \question@done as the last element. For each element we process we check whether it is that end-marker. That check simply works by gobbling everything up to the first \question@done token. If #1 doesn't contain that token the explicitly placed \question@done in the replacement text of \question will be gobbled, which leaves #1 or\question in the input stream. I omitted reading #1 #2 #3 as separate arguments, if you need that you'll have to make sure that these spaces are also part of the end marker, which is a bit more complicated, we can achieve that like this:

\documentclass{article}

\makeatletter
\@ifdefinable\question
  {%
    \def\question#1 #2 #3?%
      {\question@check#3\question@done#1 #2 #3 or\question}%
  }
\def\question@check#1\question@done{}
\def\question@done#1\question{}

\@ifdefinable\test
  {\edef\test#1{\noexpand\question#1 \space\noexpand\question@done?}}
\makeatother

\begin{document}
\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?}
\end{document}

which places two consecutive spaces in front of \question@done inside the definition of \test.


With these we still got the problem of the trailing or, because we check for end of recursion only after we placed it. This'll get a bit more tricky if we still want to step through the list one element at a time (or we use @StevenB.Segletes answer, which reads the entire list remainder for each recursion, which is a bit slower, but shouldn't be a show stopper if your lists aren't long anyways).

The approach we take here is to first expand \question twice (so \question and the first \question@check will be expanded). If the list was empty the input will now contain

\question@clean \question@done  or   \question@done \question

else the input will contain

\question@clean  orAb Bc Cd\question

So basically \question@clean should gobble two tokens if those are or, and if the first token is \question@done the input was empty. For this we just reuse \question@check.

\documentclass{article}

\makeatletter
\@ifdefinable\question
  {%
    \edef\question#1 #2 #3?%
      {%
        \noexpand\question@check#3\noexpand\question@done
        \space or#1 #2 #3\noexpand\question
      }%
  }
\def\question@check#1\question@done{}
\def\question@done#1\question{}
\def\question@clean#1{\question@check#1\question@done\@gobble}

\@ifdefinable\test
  {%
    \edef\test#1%
      {%
        \unexpanded
          {\expandafter\expandafter\expandafter\question@clean\question}%
        #1 \space\noexpand\question@done?%
      }
  }

\makeatother

\begin{document}
\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?}
\end{document}

enter image description here

2

The following is similar to @egreg's answer, but instead of breaking the input into a sequence and rebuilding a token list from that sequence it just works on the input directly via \tl_replace_all:Nnn.

This has a drawback against @egreg's answer: If you don't put a space after ? or if you put one before ? this'll get it wrong and have no space after or or a space too much in front of it. @egreg's answer will remove all spaces around the ? as well (and any leading or trailing ones), and afterwards insert them correctly ({ ~or~ }).

\documentclass[border=3.14, varwidth]{standalone}

\ExplSyntaxOn
\tl_new:N \l_vaduzstevin_questions_tl
\cs_new_protected:Npn \vaduzstevin_questions:n #1
  {
    % place a marker at the end so that we can distinguish the last ?
    \tl_set:Nn \l_vaduzstevin_questions_tl { #1 \s_stop }
    % remove the last ? if it's there
    \tl_replace_all:Nnn \l_vaduzstevin_questions_tl { ? \s_stop } { \s_stop }
    % remove the marker (we do this in two steps to also get it correct if there
    % is no trailing ? in the input)
    \tl_replace_all:Nnn \l_vaduzstevin_questions_tl { \s_stop } {}
    % replace all other ? with ' or'
    \tl_replace_all:Nnn \l_vaduzstevin_questions_tl { ? } { ~ or }
    \l_vaduzstevin_questions_tl
  }
\NewDocumentCommand \test { m }
  { \vaduzstevin_questions:n {#1} }
\ExplSyntaxOff

\begin{document}
\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?}

\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf}
\end{document}

enter image description here

1
  • @UlrichDiez thank you for worrying about me :) I'm glad I could relief you.
    – Skillmon
    Feb 24 at 20:23
1

If you like brace-hacks:

\documentclass{article}
\usepackage{trimspaces}

\makeatletter
\@ifdefinable\@gobbletoquestionmark{%
  \long\def\@gobbletoquestionmark#1?{}%
}%
\@ifdefinable\question{%
  \long\def\question#1?{%
    % #1 is the space-separated triple
    \trim@spaces@noexp{#1}%
    \expandafter\innertest\expandafter{\iffalse}\fi
  }%
}%
\newcommand\innertest[2]{%
   \ifcat$\detokenize\expandafter{\@firstoftwo#1{}{}}$%
   \expandafter\@gobble\else\expandafter\@firstofone\fi
   {%
     \iffalse{\fi#2%
        \ifcat$\detokenize\expandafter{\@gobbletoquestionmark#1?}$%
        \expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
        {\question#1?}{\question#1}%
     }%
     { or }%
  }%
}%
\newcommand\test[1]{\innertest{#1}{}}%
\makeatother

\begin{document}
(\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?})

(\test{Ab Bc Cd? Tm Br Sl? Kr Nd Zf?  })

(\test{  Ab Bc Cd? Tm Br Sl? Kr Nd Zf?})

(\test{  Ab Bc Cd? Tm Br Sl? Kr Nd Zf?  })

(\test{  Ab Bc Cd ? Tm Br Sl ? Kr Nd Zf ?  })

(\test{  Ab Bc Cd ? Tm Br Sl ? Kr Nd Zf   })

(\test{  Ab Bc Cd ? Tm Br Sl ? Kr Nd Zf})
\end{document}

enter image description here

0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .