8

I want to check whether an argument of a command is fully contained in a pair of curly braces without using any extra packages. For this I define two macros containing { and } with catcode 12, respectively. Then I turn the input into a string using \detokenize and compare the first and the last token I find.

The macro should either ignore leading and trailing spaces around the group or treat them as false (ignoring has higher priority -- two versions one ignoring one not would be best).

So the following should be outputted for the ignoring version:

\MyIfGroup{{}}{true}{false}   -> true
\MyIfGroup{ {}}{true}{false}  -> true
\MyIfGroup{{} }{true}{false}  -> true
\MyIfGroup{ {} }{true}{false} -> true
\MyIfGroup{ }{true}{false}    -> false
\MyIfGroup{a}{true}{false}    -> false
\MyIfGroup{{}a}{true}{false}  -> false

The not ignoring version should return true only for the first of the above cases. Anything inside the inner pair of braces (even nested braces) shouldn't make a difference (except for unmatched braces). The case \MyIfGroup{{}{}} is considered an edge case (by myself) and doesn't have to be detected correctly -- it would be nice though.

The following code is an ignoring version which detects everything except {}{} (that's ok) and if there is space after the pair of braces, in which case it throws a "Runaway argument"-error.

\documentclass[]{article}

\makeatletter
\newcommand\MyIfEmptyTF[1]
  {%
    \if\relax\detokenize{#1}\relax
      \expandafter\@firstoftwo
    \else
      \expandafter\@secondoftwo
    \fi
  }
\newcommand\MyIfGroupTF[1]
  {%
    \MyIfEmptyTF{#1}{\@secondoftwo}
      {%
        \expandafter\MyIfGroup@preopen\expandafter{\detokenize{#1}}%
      }%
  }
\newcommand\MyIfGroup@preopen[1]
  {%
    \MyIfDetSpaceTF{#1}
      {\@secondoftwo}
      {\MyIfGroup@open#1\end@MyIfGroup}%
  }
\long\def\MyIfGroup@open#1#2\end@MyIfGroup%
  {%
    \if\Mylbraceother#1%
      \expandafter\@firstoftwo
    \else
      \expandafter\@secondoftwo
    \fi
    {\MyIfGroup@close#2\end@MyIfGroup}
    {\@secondoftwo}%
  }
\long\def\MyIfGroup@close#1#2\end@MyIfGroup
  {%
    \MyIfEmptyTF{#2}
      {%
        \MyIfDetSpaceTF{#1}
          {\@secondoftwo}
          {%
            \if\Myrbraceother#1%
              \expandafter\@firstoftwo
            \else
              \expandafter\@secondoftwo
            \fi
          }%
      }
      {\MyIfGroup@close#2\end@MyIfGroup}
  }
\newcommand\MyIfDetSpaceTF[1]
  {%
    \if#1\detokenize{ }%
      \expandafter\@firstoftwo
    \else
      \expandafter\@secondoftwo
    \fi
  }
\bgroup
\catcode`\{=12
\catcode`\}=12
\catcode`\(=1
\catcode`\)=2
\gdef\Mylbraceother({)
\gdef\Myrbraceother(})
\egroup
\makeatother

\begin{document}
Works: \MyIfGroupTF{{i}}{true}{false}.

Works: \MyIfGroupTF{ {}}{true}{false}.

Works: \MyIfGroupTF{ }{true}{false}.

Works: \MyIfGroupTF{ a }{true}{false}.

Works: \MyIfGroupTF{a}{true}{false}.

Wrong: \MyIfGroupTF{{}{}}{true}{false}.

%Doesn't compile (fails in @close): \MyIfGroupTF{{} }{true}{false}.
\end{document}
  • 2
    Do you know about \def\whatever#1#{...}? – Henri Menke Mar 22 '18 at 20:31
  • @HenriMenke nope. I don't. 127 pages ahead of where I am in the TeXbook :( But I don't quite get (on first look) how to use it here. – Skillmon Mar 22 '18 at 20:39
5

I don't understand what you exactly want, but maybe the following code can help you. (Edit: I added new version of the code which is full expandable)

\def\MyIfGroup#1{\expandafter\MyIfGroupA\romannumeral-`\.#1.{}\end}
\def\MyIfGroupA#1#{\ifx\end#1\end\expandafter\MyIfGroupB
   \else\expandafter\MyIfGroupE\fi}
\def\MyIfGroupB#1#2#3\end{\ifx\end#3\end\expandafter\MyIfGroupT
   \else\expandafter\MyIfGroupF\fi}
\def\MyIfGroupE#1\end{\MyIfGroupF}
\def\MyIfGroupF#1#2{#2}
\def\MyIfGroupT#1#2{#1}

\tt
\MyIfGroup{{}}{true}{false}   -> true

\MyIfGroup{ {}}{true}{false}  -> true

\MyIfGroup{{} }{true}{false}  -> true

\MyIfGroup{ {} }{true}{false} -> true

\MyIfGroup{ }{true}{false}    -> false

\MyIfGroup{a}{true}{false}    -> false

\MyIfGroup{{}a}{true}{false}  -> false

\MyIfGroup{ {a} }{true}{false} -> true

\MyIfGroup{ {a}{} }{true}{false} -> false

\MyIfGroup{{}{}}{true}{false} -> false    
\bye

Edit: How it works: The potential first space is removed by \romannumeral-``\. The rest is read by \MyIfGroupA macro which reads a parameter #1 to the first occurrence of { (without it). If there is no such brace then the original parameter followed by dot is read because we added .{}\end to the input stream. This means that the #1 of \MyIfGroupA is not empty, so \MyIfGroupE is processed (this macro removes the rest from input stream and goes to the false branch. If thee exist { but something is before it then the result is the same. Else #1 of \MyIfGroupA is empty. Then the \MyIfGroupB is processed. It reads the (first) braced parameter to #1 then reads one token to #2 and the rest of input stream to \end in #3. The reading of #2 ignores potentially spaces and if there is nothing else the . is read to #2 and nothing is in #3 because outer braces are removed when #3 is read. And this is exactly the situation where we need to go to the true branch. Else there is something else after first braced parameter and we go to the false branch.

8

This doesn't ignore space, I think it's easier to use strcmp, I added one extra test for {a} at the end:

enter image description here

\documentclass{article}

\makeatletter

\def\zz#1\zz{#1}
\def\MyIfGroup#1{%
\ifnum\expandafter\pdfstrcmp\expandafter{\zz#1\zz}{#1}=0
\expandafter\@secondoftwo\else\expandafter\@firstoftwo
\fi}
\makeatother
\begin{document}


\MyIfGroup{{}}{true}{false}

\MyIfGroup{ {}}{true}{false}

\MyIfGroup{{} }{true}{false}

\MyIfGroup{ {} }{true}{false}

\MyIfGroup{ }{true}{false}

\MyIfGroup{a}{true}{false}

\MyIfGroup{{}a}{true}{false}

\MyIfGroup{{a}}{true}{false}

\end{document}
  • which engines support \pdfstrcmp? (possibly under another name) – user4686 Mar 23 '18 at 7:05
  • @jfbu All of the common ones (pdfTeX, XeTeX, LuaTeX, e-(u)pTeX): it's a core requirement of expl3 – Joseph Wright Mar 23 '18 at 7:31
  • @JosephWright very good thanks, I restrained from using \pdfstrcmp in xint (particularly obviously \xintCmp) so far because I thought XeTeX did not support it... – user4686 Mar 23 '18 at 8:26
5

Based on the expandable loop code in expl3, but as requested using no packages:

\documentclass{article}
\makeatletter
\newcommand\MyIfGroupTF[1]{%
  \MyIfGroup@loop@i#1\q@mark\q@stop
}
\long\def\MyIfGroup@loop@i#1\q@stop{%
  \MyIfGroup@if@brace{#1}%
    {\MyIfGroup@brace@i}
    {%
      \MyIfGroup@if@normal{#1}
        {\MyIfGroup@end}
        {\MyIfGroup@space@i}%
    }%
      #1\q@stop
}
\expandafter\long\expandafter\def\expandafter\MyIfGroup@space@i\space#1\q@stop{%
  \MyIfGroup@loop@i#1\q@stop
}
\newcommand\MyIfGroup@brace@i[1]{\MyIfGroup@loop@ii}
\long\def\MyIfGroup@loop@ii#1\q@stop{%
  \MyIfGroup@if@brace{#1}%
    {\MyIfGroup@end}
    {%
      \MyIfGroup@if@normal{#1}
        {\MyIfGroup@normal@ii}
        {\MyIfGroup@space@ii}%
    }%
      #1\q@stop
}
\newcommand\MyIfGroup@normal@ii[1]{%
  \ifx#1\q@mark
    \expandafter\MyIfGroup@normal@cleanup
  \else
    \expandafter\MyIfGroup@end
  \fi
}
\long\def\MyIfGroup@normal@cleanup#1\q@stop{\@firstoftwo}
\expandafter\long\expandafter\def\expandafter\MyIfGroup@space@ii\space#1\q@stop{%
  \MyIfGroup@loop@ii#1\q@stop
}

\long\def\MyIfGroup@end#1\q@stop{\@secondoftwo}


\newcommand\MyIfGroup@if@brace[1]{%
  \ifcat\expandafter\@gobble\expandafter{\expandafter{\string#1?}}**%
    \expandafter\@secondoftwo
  \else
    \expandafter\@firstoftwo
  \fi
}
\newcommand\MyIfGroup@if@normal[1]{%
  \ifcat\iffalse{\fi\MyIfGroup@if@normal@aux?#1 }%
    \expandafter\@gobble\expandafter{\expandafter{\string#1?}}**%
    \expandafter\@firstoftwo
  \else
    \expandafter\@secondoftwo
  \fi
}
\long\def\MyIfGroup@if@normal@aux#1 {%
 \if\relax\detokenize\expandafter{\@gobble#1}\relax
   ^%
 \fi
 \expandafter\@gobble\expandafter{\iffalse}\fi
}
\newcommand*\q@mark{\q@mark}
\newcommand*\q@stop{\q@stop}
\makeatother
\begin{document}

Works: \MyIfGroupTF{{i}}{true}{false}.

Works: \MyIfGroupTF{ {}}{true}{false}.

Works: \MyIfGroupTF{ }{true}{false}.

Works: \MyIfGroupTF{ a }{true}{false}.

Works: \MyIfGroupTF{a}{true}{false}.

Works: \MyIfGroupTF{{}{}}{true}{false}.

Works: \MyIfGroupTF{{} }{true}{false}.
\end{document}

The basic idea is to use a delimited argument to allow us to test the head of the input as one of space/group/'normal', then to branch as required. (See l3tl.dtx and in particular 'Token by token changes' in the code for more.) There are two loops, the first to check we have a brace group, the second to make sure it's the only one and that there are no other tokens outside of it. One could extend the code to expand any macros, though this makes life more 'interesting'.


One could also use a version of David's answer combined with a space-trimming approach (see https://tex.stackexchange.com/a/69771):

\documentclass{article}
\makeatletter
\long\def\trim@spaces#1{%
  \@@trim@spaces{\q@mark#1}%
}
\def\@tempa#1{%
  \long\def\@@trim@spaces##1{%
    \@@trim@spaces@i##1\q@nil\q@mark#1{}\q@mark
      \@@trim@spaces@ii
      \@@trim@spaces@iii
      #1\q@nil
      \@@trim@spaces@iv
      \q@stop
  }%
  \long\def\@@trim@spaces@i##1\q@mark#1##2\q@mark##3{%
    ##3%
    \@@trim@spaces@i
    \q@mark
    ##2%
    \q@mark#1{##1}%
  }%
  \long\def\@@trim@spaces@ii\@@trim@spaces@i\q@mark\q@mark##1{%
    \@@trim@spaces@iii
    ##1%
  }%
  \long\def\@@trim@spaces@iii##1#1\q@nil##2{%
    ##2%
    ##1\q@nil
    \@@trim@spaces@iii
  }%
  \long\def\@@trim@spaces@iv##1\q@nil##2\q@stop{%
    \unexpanded\expandafter{\@gobble##1}%
  }%  
}
\@tempa{ }
\def\zz#1\relax{#1}
\newcommand\MyIfGroup[1]{%
\ifnum\expandafter\pdfstrcmp\expandafter
  {\expandafter\zz\romannumeral-`0\trim@spaces{#1}\relax}%
  {\trim@spaces{#1}}%
    =0 %
\expandafter\@secondoftwo\else\expandafter\@firstoftwo
\fi}
\begin{document}

\MyIfGroup{{}}{true}{false}

\MyIfGroup{ {}}{true}{false}

\MyIfGroup{{} }{true}{false}

\MyIfGroup{ {} }{true}{false}

\MyIfGroup{ }{true}{false}

\MyIfGroup{a}{true}{false}

\MyIfGroup{{}a}{true}{false}

\MyIfGroup{{a}}{true}{false}

\end{document}
3

This ignores spaces (but doesn't deal with the edge case):

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\MyIfGroup}{mmm}
 {
  \regex_match:nnTF { \A\s*\cB\{.*\cE\}\s*\Z } { #1 } { #2 } { #3 }
 }
\ExplSyntaxOff

\begin{document}

\MyIfGroup{{}}{true}{false}   $\to$ true

\MyIfGroup{ {}}{true}{false}  $\to$ true

\MyIfGroup{{} }{true}{false}  $\to$ true

\MyIfGroup{ {} }{true}{false} $\to$ true

\MyIfGroup{ }{true}{false}    $\to$ false

\MyIfGroup{a}{true}{false}    $\to$ false

\MyIfGroup{{}a}{true}{false}  $\to$ false

\end{document}

enter image description here

If you want to deal with the edge case and make it return false:

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\MyIfGroup}{mmm}
 {
  \regex_match:nnTF { \A\s*\cB\{.*\cE\}\s*\Z } { #1 }
   {
    \int_compare:nNnTF { \tl_count:n { #1 } } > { 1 } { #3 } { #2 }
   }
   { #3 }
 }
\ExplSyntaxOff

\begin{document}

\MyIfGroup{{}}{true}{false}   $\to$ true

\MyIfGroup{ {}}{true}{false}  $\to$ true

\MyIfGroup{{} }{true}{false}  $\to$ true

\MyIfGroup{ {} }{true}{false} $\to$ true

\MyIfGroup{ }{true}{false}    $\to$ false

\MyIfGroup{a}{true}{false}    $\to$ false

\MyIfGroup{{}a}{true}{false}  $\to$ false

\MyIfGroup{{}{}}{true}{false} $\to$ false

\end{document}

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.