2

I want to test if a string contains a substring. I finded good answers in this question. But I encounter a problem with the first answer of egreg using expl3 and I don't get the same problem with Werner's answer. The problem comes when I use the macro in \setlength (I got a 'Missing number, treated' as zero error). I would like to know why and if it can be adapt. I got the same error with Werner's answer buy putting \protect before \instring. Is \instring fragile and \instringTF robust? Is it the reason why one works and the other don't? I may be completly wrong about that. I'm kind of suprise that the problem would come from the robust one, I tought it was usually the other way around.

\documentclass{article}
\usepackage{xparse}

%egreg answer
\ExplSyntaxOn
\NewDocumentCommand{\instringTF}{mmmm}
{%
  \oleks_instring:nnnn {#1}{#2}{#3}{#4}
}%
\tl_new:N \l__oleks_instring_test_tl
\cs_new_protected:Nn \oleks_instring:nnnn
{%
  \tl_set:Nn \l__oleks_instring_test_tl {#1}
  \regex_match:nnTF {\u{l__oleks_instring_test_tl}}{#2}{#3}{#4}
}%
\ExplSyntaxOff

%Werner answer
\newcommand{\instring}[4]{%
  \ifnum\pdfmatch{#1}{#2}=1
    #3%
  \else
    #4%
  \fi
}

\begin{document}

The sky is \instringTF{str}{string}{blue}{green}. %working
The sky is \instring{str}{string}{blue}{green}.\\ %working

With instringTF (expl3) from egreg:
\begin{itemize}
  %\setlength{\itemsep}{\instringTF{str}{string}{1cm}{2cm}}   %not working
  \item first item
  \item second item
\end{itemize}

With instring (pdfmatch) from Werner:
\begin{itemize}
  \setlength{\itemsep}{\instring{str}{string}{1cm}{2cm}}    %working
  \item first item
  \item second item
\end{itemize}

\end{document}
1
  • There has been discussion about using \pdfmatch in expl3, but at the end it was decided not to employ it, because it has nonstandard syntax and is not implemented in all TeX engines.
    – egreg
    Commented Oct 14, 2020 at 11:53

1 Answer 1

4

The second argument of \setlength is supposed to be readable as a ⟨glue⟩ according to TeX's grammar. A ⟨glue⟩ is read by TeX using expansion only, therefore any command used there must be “expandable”1 (and the end result of recursive expansion must be a ⟨glue⟩). Werner's technique relies on TeX's \ifnum and pdfTeX's \pdfmatch primitives, both of which satisfy this constraint, whereas egreg's technique uses \tl_set:Nn and \regex_match:nnTF, none of which is expandable (using the analogy from the TeXbook, they both use low-level commands that need to be processed in TeX's stomach, as opposed to its mouth).

It is possible, however, to split egreg's command into two parts: one that requires expansion + execution (i.e., mouth + stomach); the other which only requires expansion (what happens in TeX's mouth). In the code below:

  • The former is called \testIfInString and performs the actual substring test. It can't be used in expansion-only contexts, such as in the second argument of \setlength, but you can usually find a suitable place before where it will work just fine.

  • The latter is fully expandable and simply chooses the correct branch according to the outcome of the last use of \testIfInString. This second command is called \ifInStringTF, and I provide variants \ifInStringT and \ifInStringF for your comfort. \ifInStringTF, \ifInStringT and \ifInStringF are safe to use in expansion-only contexts, such as: in the second argument of \setlength, in the second argument of \setcounter, after \if, \ifcat \ifnum, \ifdim, \ifodd, \ifcase, \number, \romannumeral, inside the replacement text of an \edef or an \xdef, and many other places.

\documentclass{article}
\usepackage{xparse}
\usepackage{multicol}           % only for the demo code

\ExplSyntaxOn
\NewDocumentCommand { \testIfInString } { m m }
  {
    \oleks_test_if_in_string:nn {#1} {#2}
  }

\NewExpandableDocumentCommand { \ifInStringTF } { }
  { \oleks_if_in_string:TF }

\NewExpandableDocumentCommand { \ifInStringT } { }
  { \oleks_if_in_string:T }

\NewExpandableDocumentCommand { \ifInStringF } { }
  { \oleks_if_in_string:F }

\tl_new:N \l__oleks_instring_test_tl
\bool_new:N \l__oleks_is_in_string_bool

\cs_new_protected:Nn \oleks_test_if_in_string:nn
  {
    \tl_set:Nn \l__oleks_instring_test_tl {#1}
    \regex_match:nnTF { \u{l__oleks_instring_test_tl} } {#2}
      { \bool_set_true:N \l__oleks_is_in_string_bool }
      { \bool_set_false:N \l__oleks_is_in_string_bool }
  }

\prg_new_conditional:Npnn \oleks_if_in_string: { T, F, TF } % you can also add 'p'
  {
    \bool_if:NTF \l__oleks_is_in_string_bool
      { \prg_return_true: }
      { \prg_return_false: }
  }
\ExplSyntaxOff

\begin{document}

\testIfInString{str}{string}%
The sky is \ifInStringTF{blue}{green}. \ifInStringT{A}\ifInStringF{B}.
\testIfInString{foobar}{string}%
The sky is not \ifInStringTF{blue}{green}. \ifInStringT{A}\ifInStringF{B}.

With \verb|\setlength|:
\begin{multicols}{2}
\raggedcolumns % we don't want the first column material to be stretched
\begin{itemize}
  \testIfInString{str}{string}%
  \setlength{\itemsep}{\ifInStringTF{1cm}{2cm}}
  \item first item
  \item second item
\end{itemize}

\columnbreak
\begin{itemize}
  \testIfInString{not in 'string'}{string}%
  \setlength{\itemsep}{\ifInStringTF{1cm}{2cm}}
  \item first item
  \item second item
\end{itemize}
\end{multicols}

\end{document}

enter image description here

Improvement of Werner's \instring macro

For reasons that I explained yesterday, should you decide for Werner's approach, I recommend to use a modified version of his \instring macro. The modified version is called \MyInString in the following example.

\documentclass{article}

% From Werner in <https://tex.stackexchange.com/a/446691/73317>
\newcommand{\instring}[4]{%
  % \instring{<pattern>}{<string>}{<true>}{<false>}
  %   pattern = sought after string
  %   string  = string to search
  \ifnum\pdfmatch{#1}{#2}=1
    #3%
  \else
    #4%
  \fi
}

\makeatletter
% I suggest this instead
\newcommand{\MyInString}[2]{%
  \ifnum\pdfmatch{#1}{#2}=1
    \expandafter\@firstoftwo
  \else
    \expandafter\@secondoftwo
  \fi
}
\makeatother

\begin{document}

\newcommand{\mystring}{abcdef}

\instring{a}{abcdef}{Yes}{No} \MyInString{a}{abcdef}{Yes}{No}% Yes

\instring{abc}{\mystring}{Yes}{No} \MyInString{abc}{\mystring}{Yes}{No}% Yes

\instring{acb}{\mystring}{Yes}{No} \MyInString{acb}{\mystring}{Yes}{No}% No

\medskip
%\instring{cde}{abcdef}{\textbf}{\textit}{some text}% ERROR: Too many }'s.
%\instring{zzz}{abcdef}{\textbf}{\textit}{other text}% ERROR: Too many }'s.
\MyInString{cde}{abcdef}{\textbf}{\textit}{some text} % bold
\MyInString{zzz}{abcdef}{\textbf}{\textit}{other text}% italics

\end{document}

enter image description here

Note: the two approaches are semantically very different: with \regex_match:nnTF { \u{l__oleks_instring_test_tl} } ..., one performs a precise substring test (where “string” is really “token list”). The tokens in \l__oleks_instring_test_tl have to match exactly, one by one, for the test to be successful. In contrast to this, the method based on \ifnum\pdfmatch{#1}{#2}=1 performs a regular expression match: #1 is interpreted as a POSIX extended regular expression. This implies that, for instance, \MyInString{(abc)+de}{zzabcabcabcdef}{yes}{no} expands to yes because in zzabcabcabcdef, there is abc one or more times, immediately followed by de.


Footnote

  1. One sometimes says “fully expandable” for the same thing.

You must log in to answer this question.

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