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 ;-)

6 Answers 6

16
+100

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.

12
  • Thanks! Very interesting use of \csname! Again something I wouldn't have thought off. May 22, 2012 at 14:08
  • Why #6#7 and the \ifx#6!!\fi?
    – egreg
    May 22, 2012 at 14:17
  • @egreg take it out and watch what happens May 22, 2012 at 14:20
  • I did: the output is the same. As long as there are at least five ! after \meaning#1 there should be no problem.
    – egreg
    May 22, 2012 at 14:25
  • @egrep if you take it out \skip will generate victory, no? May 22, 2012 at 14:38
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}
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}
3
  • I like this idea. I wouldn't thought about it myself. However, the fact that the macro is typeset etc. is a drawback. May 22, 2012 at 11:03
  • it would break for quite a number of cases so it isn't something that you can use with arbitrary macro input May 22, 2012 at 13:49
  • Another problem is that this requires fonts to be loaded. If your current font is \nullfont all boxes will have zero width. Dec 1, 2019 at 6:04
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).

5
  • Did you look at using the \token_if... tests?
    – Joseph Wright
    May 22, 2012 at 16:02
  • @JosephWright No; but they are too easy! :)
    – egreg
    May 22, 2012 at 16:04
  • 1
    Nice example of showing the power of expl3! May 22, 2012 at 16:15
  • @FrankMittelbach Do you agree with the form I used, particularly with respect to \NewDocumentCommand?
    – egreg
    May 22, 2012 at 16:17
  • @egrep yes I do. Clear separation of the functionality provided, i.e., \lentest_if_length:NTF and the document interface (here using 2e syntax via xparse). May 22, 2012 at 19:48
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}
3
  • 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, 2012 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. May 23, 2012 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, 2012 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}
3
  • 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, 2018 at 7:27
  • @daleif: I tried that and it was inserting a " before the name. Mar 29, 2018 at 7: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, 2018 at 7:32

You must log in to answer this question.

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