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}
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}
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
- One sometimes says “fully expandable” for the same thing.
\pdfmatch
inexpl3
, but at the end it was decided not to employ it, because it has nonstandard syntax and is not implemented in all TeX engines.