26

I'm in need of a macro which receives a control sequence as argument and needs to branch if this macro is actually a length (of any kind, e.g. dimen, skip etc.) or "just" a normal macro (including one which is not defined yet).

\def\assignlengthormacro#1#2{%
   \@iflength{#1}{%
      \setlength{#1}{#2}%  or simply #1=#2\relax
   }{%
      \def#1{#2}%
   }%
}

\newlength\mylength % LaTeX length => TeX skip register
\newdimen\mydimen
\def\mymacro{Some other definition}
\assignlengthormacro{\mylength}{1pt}
\assignlengthormacro{\mydimen}{1pt}
\assignlengthormacro{\mymacro}{1pt}
\assignlengthormacro{\previouslyundefinedmacro}{1pt}% should still work

Note: I'm able to code it myself (using \meaning), but I think it is a good question and like to see other possible approaches. Maybe other high-rep users will wait a little and give others a chance to try it and earn some reputation points. Bonus challenge: make the test fully expandable ;-)

egreg
  • 1,121,712
Martin Scharrer
  • 262,582

6 Answers6

16

Here is an expandable test for "skips" it could be extended to dimens in a similar manner:

\def\grabfive #1#2#3#4#5#6#7\grabfive{#2#3#4#5\ifx#6!!\fi}

\def\testforskip#1{%
\expandafter\ifx\csname \expandafter\grabfive\meaning#1!!!!!\grabfive\endcsname\skip
   \typeout{\string #1 victory}%
\else
   \typeout{\string#1 fail =\meaning#1}%
\fi}

\newskip\boo

\testforskip\foo
\testforskip\boo
\testforskip\skip
\testforskip\skipit
\def\skipthat{}
\testforskip\skipthat

This would give you:

\foo fail =undefined
\boo victory
\skip fail =\skip
\skipit fail =undefined
\skipthat fail =macro:->

My claim would be that there isn't anything expandable + safe that doesn't involve \meaning.

13

Here is a purely expandable non-expl3 solution for everything. I welcome suggestions of its failure modes. Because of the behaviour of the \meaning of \count, \skip, \dimen and \toks-defined registers, an expandable test for primitives isn't as straightforward as it might first seem.

\documentclass{article}
\makeatletter
\def\@@empty{\@gobble\@@empty}
\def\if@cond#1\fi{\csname @#1\@@empty first\else second\fi oftwo\endcsname}
\def\if@blank#1{\if@cond\ifcat$\detokenize\expandafter{\@gobble#1.}$\fi}
\def\if@num#1#{\if@cond\ifnum#1\fi}
\def\if@cseq#1#2{\if@cond\ifx#1#2\fi}
\begingroup
\catcode`\&=7
\gdef\defregistertester#1#2{%
  \begingroup
  \def\x##1{\unexpanded\expandafter
    {\csname\expandafter\@gobble\string#1@test@##1\endcsname}}%
  \edef\x{\endgroup
    \def\noexpand#2####1{%
      \unexpanded{\if@cseq#1##1\register@test@a}{%
        \unexpanded{\ifprimitive{##1}\register@test@b}
        {\noexpand\expandafter\x{a}\noexpand\meaning####1:&}%
      }%
    }%
    \def\x{a}####1:####2&{\x{b}####1\string#1&}%
    \def\x{b}####1\string#1####2&{\noexpand\if@blank{####1}}%
  }\x
}
\endgroup
\def\register@test@a#1#2{#2}
\def\register@test@b#1#2{is primitive}
\def\ifprimitive#1{\expandafter\if@primitive\meaning#1\relax}
\def\if@primitive#1#2\relax{%
  \if@cond\if#1\@backslashchar\fi{%
    \ifdigitfound{#2}\@secondoftwo\@firstoftwo
  }{%
    \@secondoftwo
  }%
}
\def\ifdigitfound#1{\if@cond\if0\if@blank{#1}{1}{\if@digitfound#1\@nnil}\fi}
\def\if@digitfound#1{%
  \if@cseq#1\@nnil{1}{%
    \if@num`#1>47{%
      \if@num`#1<58{0\remove@to@nnil}{\if@digitfound}%
    }{%
      \if@digitfound
    }%
  }%
}

% Tests
\newskip\skipa
\newcount\cnta
\newdimen\dima
\newtoks\toksa

\defregistertester\skip\ifskip
\defregistertester\count\ifcount
\defregistertester\dimen\ifdimen
\defregistertester\toks\iftoks

\edef\x{\ifdimen\textwidth{T}{F}} % -> True
\edef\x{\ifdimen\hsize{T}{F}} % -> is primitive
\edef\x{\ifdimen\dima{T}{F}} % -> True
\edef\x{\ifskip\muskip{T}{F}} % -> is primitive
\edef\x{\ifskip\foo{T}{F}}
\edef\x{\ifskip\skipa{T}{F}}
\edef\x{\ifskip\skip{T}{F}}
\edef\x{\ifskip\skipit{T}{F}}
\def\skipx{}
\edef\x{\ifskip\skipx{T}{F}}
\makeatother

\begin{document}
\textbf{Primitive tests}\par

True: \ifdigitfound{a1c}{T}{F}\par
False: \ifdigitfound{abc}{T}{F}\par
True: \ifprimitive\muskip{T}{F}\par
True: \ifprimitive\hsize{T}{F}\par
False: \ifprimitive\textwidth{T}{F}\par
False: \ifprimitive\martin{T}{F}\par
True: \ifprimitive\toks{T}{F}\par
False: \ifprimitive\toksa{T}{F}\par

\par\medskip
\textbf{Skip tests}\par\medskip

False: \ifskip\foo{T}{F}\par
True: \ifskip\skipa{T}{F}\par
False: \ifskip\skip{T}{F}\par
False: \ifskip\skipx{T}{F}

\par\medskip
\textbf{Count tests}\par\medskip

False: \ifcount\foo{T}{F}\par
True: \ifcount\cnta{T}{F}\par
False: \ifcount\skipa{T}{F}\par
\def\countx{}
False: \ifcount\countx{T}{F}\par

\par\medskip
\textbf{Dimension tests}\par\medskip

False (is primitive): \ifdimen\hsize{T}{F}\par
True: \ifdimen\textwidth{T}{F}\par
True: \ifdimen\dima{T}{F}

\par\medskip
\textbf{Toks tests}\par\medskip

True: \iftoks\toksa{T}{F}\par
False: \iftoks\toks{T}{F}\par
False: \iftoks\toksb{T}{F}\par
\end{document}
Ahmed Musa
  • 11,742
12

A simple measurment of an \hbox should work. Unfortunately, this method may not work when \mymcro requires argument(s).

\documentclass{article}
\makeatletter
\def\iflength#1{%
    \begingroup
    \setbox\z@\hbox{\ifdefined#1\expandafter#1\fi0pt }%
    \ifdim\wd\z@=\z@\endgroup\expandafter\@firstoftwo
    \else\endgroup\expandafter\@secondoftwo
    \fi}
\makeatother
\newdimen\mydimen
\newskip\myskip
\def\mymacro{dummy code}% must not have argument
\begin{document}
\iflength\mydimen{true}{false}

\iflength\myskip{true}{false}

\iflength\mymacro{true}{false}

\iflength\undefmacro{true}{false}
\end{document}
unbonpetit
  • 6,190
  • 1
  • 20
  • 26
10

Let me start with an unexpandable version:

\documentclass{article}
\usepackage{xparse,l3regex}
\ExplSyntaxOn
\NewDocumentCommand{\IfLengthTF}{mmm}
 {
  \lentest_if_length:NTF #1 { #2 } { #3 }
 }
\NewDocumentCommand{\IfLengthF}{mm}
 {
  \lentest_if_length:NF #1 { #2 }
 }
\NewDocumentCommand{\IfLengthT}{mm}
 {
  \lentest_if_length:NT #1 { #2 }
 }
\prg_new_protected_conditional:Npnn \lentest_if_length:N #1 {p,T,F,TF}
 {
  \regex_match:nxTF {\A\\??(skip|dimen).+} { \token_to_meaning:N #1 }
   { \prg_return_true: }
   { \prg_return_false: }
 }
\cs_generate_variant:Nn \regex_match:nnTF {nx}
\ExplSyntaxOff

\newlength\pippo
\newdimen\pluto

\IfLengthTF{\pippo}{\typeout{It's a length}}{\typeout{It's NOT a length}}
\IfLengthTF{\pluto}{\typeout{It's a length}}{\typeout{It's NOT a length}}
\IfLengthTF{\skip}{\typeout{It's a length}}{\typeout{It's NOT a length}}
\IfLengthTF{\xxeey}{\typeout{It's a length}}{\typeout{It's NOT a length}}

The output is

It's a length
It's a length
It's NOT a length
It's NOT a length

One can also use \IfLengthT or \IfLengthF, when the true or false branches are not needed.

The regex matches \skip or \dimen, followed by at least one character, at the start of the argument's meaning. The \\?? means that the backslash may not be present (it happens if \escapechar is -1; other possibilities for the value of \escapechar are not taken care of, but they could if needed).


Now an expandable test (thanks to Joseph Wright for suggesting it):

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\IfLengthTF}{mmm}
 {
  \lentest_if_length:NTF #1 { #2 } { #3 }
 }
\NewDocumentCommand{\IfLengthF}{mm}
 {
  \lentest_if_length:NF #1 { #2 }
 }
\NewDocumentCommand{\IfLengthT}{mm}
 {
  \lentest_if_length:NT #1 { #2 }
 }

\prg_new_conditional:Npnn \lentest_if_length:N #1 {p,T,F,TF}
 {
  \bool_if:nTF
   {
    \token_if_dim_register_p:N #1 || \token_if_skip_register_p:N #1
   }
   { \prg_return_true: }
   { \prg_return_false: }
 }
\ExplSyntaxOff

Of course the \IFLength... commands are not expandable; but the \lentest_if_length:NTF test and its siblings are.


Why choose one or the other? It depends on what's the purpose of the macros. The second version can be made expandable, which might be desirable in certain contexts. The first version can be generalized in various ways, testing for whatever is necessary with a suitable regular expression.


Caveat: these tests don't distinguish among control sequences which are not symbolic names for skip or dimen registers and other control sequences. So \IfLengthTF{\hsize}{...}{...} will choose the "false" branch. It's impossible to recognize an internal dimen or skip register only via \meaning: one way might be to check the meaning against a list of the primitive names of the parameters (which might be added to the first method).

egreg
  • 1,121,712
3

Thanks a lot for all the answers. I also like to show how I approached it. For a non-expandable version I would expanded the \meaning and read a predefined number of character to compare it with \ifx with a \edef\SKIP{\string\skip}. This can be repeated for several different values.

In order to keep it expandable every character can be compared using its char-code. This is done using \ifnum`, because \if also compares the catcode which is set to other not letter. This is basically a state-machine. At the end I check if a digits follows to exclude primitives with the same starting letters (if there are any) or \dimen and \skip itself (thanks to Frank Mittelbach who pointed this possibility out in his answer).


Here is the approach with one macro per letter. It assumes that \escapechar is non-negative, which is usually the case.

\def\@iflength#1{%
    \ifcase0\expandafter\@iflength@a\meaning#1\@nnil\space
        \expandafter\@firstoftwo
    \else
        \expandafter\@secondoftwo
    \fi
}

\def\@iflength@a#1{%
    \ifnum\escapechar=`#1\space
        \expandafter\@iflength@b
    \else
        1%
    \fi
}
\def\@iflength@b#1{%
    \ifcase0%
        \ifx#1\@nnil 0\else
        \ifnum`d=`#1 1\else
        \ifnum`s=`#1 2\fi\fi\fi
    \space
        1%
    \or
        \expandafter\@iflength@di
    \else
        \expandafter\@iflength@sk
    \fi
}
\def\@iflength@sk#1{%
    1%
}
\def\@iflength@di#1{%
    \ifcase0%
        \ifx#1\@nnil 0\else
        \ifnum`i=`#1 1\fi\fi
    \space
        1%
    \or
        \expandafter\@iflength@dim
    \fi
}
\def\@iflength@dim#1{%
    \ifcase0%
        \ifx#1\@nnil 0\else
        \ifnum`m=`#1 1\fi\fi
    \space
        1%
    \or
        \expandafter\@iflength@dime
    \fi
}
\def\@iflength@dime#1{%
    \ifcase0%
        \ifx#1\@nnil 0\else
        \ifnum`e=`#1 1\fi\fi
    \space
        1%
    \or
        \expandafter\@iflength@dimen
    \fi
}
\def\@iflength@dimen#1{%
    \ifcase0%
        \ifx#1\@nnil 0\else
        \ifnum`n=`#1 1\fi\fi
    \space
        1%
    \or
        \expandafter\@iflength@dimen@
    \fi
}
\def\@iflength@dimen@#1{%
    \ifcase0%
        \ifx#1\@nnil 0\else
        \ifnum47<`#1 \ifnum58>`#1 1\fi\fi\fi
    \space
        1%
    \or
        0%
        \expandafter\remove@to@nnil
    \fi
}
\def\@iflength@sk#1{%
    \ifcase0%
        \ifx#1\@nnil 0\else
        \ifnum`k=`#1 1\fi\fi
    \space
        1%
    \or
        \expandafter\@iflength@ski
    \fi
}
\def\@iflength@ski#1{%
    \ifcase0%
        \ifx#1\@nnil 0\else
        \ifnum`i=`#1 1\fi\fi
    \space
        1%
    \or
        \expandafter\@iflength@skip
    \fi
}
\def\@iflength@skip#1{%
    \ifcase0%
        \ifx#1\@nnil 0\else
        \ifnum`p=`#1 1\fi\fi
    \space
        1%
    \or
        \expandafter\@iflength@skip@
    \fi
}
\def\@iflength@skip@#1{%
    \ifcase0%
        \ifx#1\@nnil 0\else
        \ifnum47<`#1 \ifnum58>`#1 1\fi\fi\fi
    \space
        1%
    \or
        0%
        \expandafter\remove@to@nnil
    \fi
}

I then coded a general macro for the repeated comparison. I guess the parse macro of LaTeX3 used in egreg's answer works similar, at least in principle.

Some test code is also included.

\documentclass{article}

\makeatletter

\def\@iflength#1{%
    \ifcase0\expandafter\@iflength@a\meaning#1\@nnil\space
        \expandafter\@firstoftwo
    \else
        \expandafter\@secondoftwo
    \fi
}

\def\@iflength@a#1{%
    \ifnum\escapechar=`#1\space
        \expandafter\@iflength@b
    \else
        1%
        \expandafter\remove@to@nnil
    \fi
}
\def\@skip@or@fi#1\or#2\fi{#1\fi}
\def\@iflength@b#1{%
    \ifcase0%
        \ifx#1\@nnil 0\else
        \ifnum`d=`#1 1\else
        \ifnum`s=`#1 2\else
        \ifnum`m=`#1 3\fi\fi\fi\fi
    \space
        1%
    \or
        \@skip@or@fi
        \@iflength@cmp imen\relax
    \or
        \@skip@or@fi
        \@iflength@cmp kip\relax
    \or
        \@iflength@cmp uskip\relax
    \fi
}
\def\@checkdigit#1{%
    \ifcase0%
        \ifnum`0>`#1 1\else
        \ifnum`9<`#1 1\fi\fi
    \space
        1%
    \else
        0%
    \fi
}
\def\@iflength@cmp#1#2\fi#3{%
    \fi
    \ifcase0%
        \ifx#3\@nnil  0\else
        \ifx#1\relax \@checkdigit#3\else
        \ifnum`#1=`#3 2\fi\fi\fi
    \space
        1%
    \or
        0%
        \expandafter\remove@to@nnil
    \or
        \@iflength@cmp#2%
    \fi
}

% \skip
% \dimen
%
\def\@expand#1{%
  \par\string#1: \@iflength{#1}{\the}{}#1%
}

\begin{document}

\def\test#1{\@iflength{#1}{\typeout{\string#1: is a length}}{\typeout{\string#1: is NOT a length (\meaning#1)}}}
%\def\test#1{\edef\A{\@iflength{#1}{YES}{No}}\A\show\A}

\test{\test}
\test{\@gobble}
\test{\@nil}
\test{\@@nil}
\test{\textwidth}
\newlength\mylength
\test{\mylength}
\def\mymacro#1{test #1}
\test{\mymacro}
\test{\skip}
\test{\dimen}
\muskipdef\mymuskip=1
\test{\muskip}
\test{\mymuskip}
\def\mymacro{1pt}

\@expand\textwidth

\@expand\mymacro

\makeatother
\end{document}
egreg
  • 1,121,712
Martin Scharrer
  • 262,582
  • I've corrected a missing i and the \muskipdef\mymuskip=1pt. The first was a typo, the second a real error: the syntax is \muskipdef\cs=<number>. In you case pt was simply printed. However, you can't assign a dimension in points to a \muskip register. – egreg May 23 '12 at 15:09
  • @egreg: Thanks, I didn't know \muskip much and thought it works similar to \skip. In this case I should rather remove it. – Martin Scharrer May 23 '12 at 15:20
  • In e-TeX one can compare \muskip registers with \dimen or \skip registers, but it's a very crude comparison via \gluetomu or \mutoglue that convert 1pt to 1mu and vice versa. – egreg May 23 '12 at 15:26
1

This now available with the etoolbox package with \ifdefdimen and \ifdeflength:

enter image description here

Code:

\documentclass{article}
\usepackage{etoolbox}

\newcommand*{\IfLengthTF}[3]{% {\ttfamily\string#1} \ifdefdimen{#1}{#2}{% \ifdeflength{#1}{#2}{#3}% }% }

\newlength\LengthRegister \newdimen\DimenRegister \newskip\SkipRegister

\begin{document} \IfLengthTF{\LengthRegister}{is a length}{is NOT a length}\par \IfLengthTF{\DimenRegister}{is a length}{is NOT a length}\par \IfLengthTF{\SkipRegister}{is a length}{is NOT a length}\par \IfLengthTF{\UndefinedMacro}{is a length}{is NOT a length}\par \end{document}

Peter Grill
  • 223,288
  • Just wondering, do you really need expl3 for the cs to string, wouldn't \string#1 be enough? (untested, not at pc) – daleif Mar 29 '18 at 07:27
  • @daleif: I tried that and it was inserting a " before the name. – Peter Grill Mar 29 '18 at 07:30
  • 1
    @PeterGrill That has to do with font encoding. If you use \ttfamily it will print \ , and if you load \usepackage[T1]{fontenc} it will show also the backslash. – Manuel Mar 29 '18 at 07:32