22

With TikZ using the calc library you can write things along the lines of

\node (A) at ($(0,0)!0.75!(1,1)$) {};

or you can write something (rather different)

\node (B) at ($(0,0)!2in!(1,1)$) {};

What I would like to do is create a command that takes an argument which is either a scalar or a dimension.

Naively I would like to do something like

\def\test#1{\ifdim#1\relax ... \else ... \fi}

Of course, that is completely wrong since \ifdim compares the values of two dimensions. So, what I really would like is something more like

\def\test#1{\ifIsDimension#1\relax ... \else ... \fi}

Any suggestions how I go about this?

A.Ellett
  • 50,533
  • Hmm... I think you are trying to answer my deleted question. :-) – kiss my armpit Oct 14 '13 at 05:50
  • Why don't you let TikZ take care what #1 is? Or is it just an example? – percusse Oct 14 '13 at 06:45
  • @percusse I really just meant the TikZ code to be illustrative of a situation in which a macro is able to discern whether a passed value is a dimension or not and subsequently modify its own behavior. – A.Ellett Oct 14 '13 at 15:00
  • 1
    @Marienplatz Perhaps then you'd be interested in the answer I came up with after seeing what others had suggested. – A.Ellett Oct 14 '13 at 16:33

7 Answers7

28

If you don't mind the overhead of the pgfmath parser you can parse the number and check \ifpgfmathunitsdeclared. This is true if a TeX unit is specified at any point in the expression, or if the expression contains something that TeX regards has having units, such as skip, and box dimensions.

\documentclass[border=0.125cm]{standalone}

\usepackage{pgfmath,pgffor}
\parindent0pt

\def\print#1{\expandafter\Print#1@}
\def\Print#1{\if#1@\else\string#1\relax\expandafter\Print\fi}

\begin{document}

\newcount\mycount
\newdimen\mydimen
\newskip\myskip
\mycount=1
\mydimen=1pt
\myskip=1pt plus 1pt
\newbox\mybox
\setbox\mybox=\hbox{1}

\begin{minipage}{3in}
\foreach \value in {1, 1.0, 1e0, sin(1), 1cm, 1pt, 1mm, 1sp, 1mu, sin(1pt), 1+1pt,
    \mycount, \mydimen, \myskip, \wd\mybox, \mycount+1pt, width("1")}{%
    \pgfmathparse{\value}
    Expression \hbox to 2.5cm{\hfill`{\tt\print\value}'}
    \ifpgfmathunitsdeclared
        \emph{\bfseries has} a unit
    \else
        has no units
    \fi

}
\end{minipage}
\end{document}

enter image description here

It's also worth noting that there is a macro \pgfmathpostparse which is executed after the parser has finished but just before it exits in which further stuff an be done. Initially it is set to \relax but is advisable to check its value in case some library or other package changes it.

The result of the parse will be in \pgfmathresult and it is possible to change it (if one really wanted to). It is however still inside a TeX group so \global must be used if the result of some test is required.

The following example using a crude integer test is not great as the parser rarely returns integers (there are a few exceptions shown below) but illustrates how it can be used.

\documentclass[border=0.125cm]{standalone}
\usepackage{pgfmath,pgffor}
\parindent0pt

\makeatletter
\newif\ifpgfmathresultinteger
\def\pgfmathpostparse{%
    \expandafter\pgfutil@in@\expandafter.\expandafter{\pgfmathresult}%
    \ifpgfutil@in@%
        \global\pgfmathresultintegerfalse%
    \else%
        \global\pgfmathresultintegertrue%
    \fi%
}

\def\print#1{\expandafter\Print#1@}
\def\Print#1{\if#1@\else\string#1\relax\expandafter\Print\fi}


\begin{document}

\newcount\mycount
\newdimen\mydimen

\mycount=1
\mydimen=1pt

\begin{minipage}{3.5in}
\foreach \value in {1, int(1.0), \mycount, \mydimen, int(\mydimen+1pt), \mycount+1pt}{%
    \pgfmathparse{\value}
    Parsing \hbox to 3.5cm{\hfill`{\tt\print\value}'} 
    does
    \ifpgfmathresultinteger
    \else
        \emph{\bfseries not} 
    \fi
    give an integer

}
\end{minipage}

\end{document}

enter image description here

Mark Wibrow
  • 70,437
  • Thank you. I'll accept this as the answer even though I've developed my own solution. Your answer got me pointed in the right direction. – A.Ellett Oct 14 '13 at 16:36
  • 1
    As @Mark said, the problem with this approach to the integer question is, annoyingly enough, \pgfmathparse{1+1} sets \pgfmathresult to 2.0. Because of this, your code gives the undesirable result "'1+1' does not give an integer". In fact, there are only two conditions in which it will say the result is an integer: either no math was done, as in \pgfmathparse{12}, or the outermost operation was surrounded by int, as in \pgfmathparse{int(1+1)} but not \pgfmathparse{int(1+1)+1}. I wrote a more complicated check that gives true if the result is either n.0 or n, for an integer n. – Hood Chatham Dec 31 '15 at 23:55
11

This is of course slow, but should take care of all the cases (number, dimension, wrong input).

\documentclass{article}
\usepackage{xparse,l3regex}
\ExplSyntaxOn
\NewDocumentCommand{\IfIsDim}{mmm}
 {
  \aellet_ifisdim:nnn { #1 } { #2 } { #3 }
 }

\tl_new:N \l_aellet_ifisdim_arg_tl
\bool_new:N \l_aellet_ifisdim_has_unit_bool
\regex_const:Nn \c_aellet_unit_regex { (pt|pc|in|bp|cm|mm|dd|cc|sp|em|ex|px)\s*\Z }
\regex_const:Nn \c_aellet_number_regex { \A\s*(\+|\-)*[0-9]*\.?[0-9]*\s*\Z }
\cs_new_protected:Npn \aellet_ifisdim:nnn #1 #2 #3
 {
  \tl_set:Nn \l_aellet_ifisdim_arg_tl { #1 }
  \regex_match:NnTF \c_aellet_unit_regex { #1 }
   {% there is a unit of measure at the end
    \bool_set_true:N \l_aellet_ifisdim_has_unit_bool
    \regex_replace_once:NnN \c_aellet_unit_regex { } \l_aellet_ifisdim_arg_tl
   }
   {% no unit of measure
    \bool_set_false:N \l_aellet_ifisdim_has_unit_bool
   }
  \regex_match:NVTF \c_aellet_number_regex \l_aellet_ifisdim_arg_tl
   {
    \bool_if:NTF \l_aellet_ifisdim_has_unit_bool { #2 } { #3 }
   }
   {
    \ERROR
   }
 }
\cs_generate_variant:Nn \regex_match:NnTF { NV }
\ExplSyntaxOff

\IfIsDim{3.5pt}{\typeout{IS DIM}}{\typeout{IS NUMBER}}
\IfIsDim{3.5}{\typeout{IS DIM}}{\typeout{IS NUMBER}}
\IfIsDim{-3}{\typeout{IS DIM}}{\typeout{IS NUMBER}}
\IfIsDim{+--.5}{\typeout{IS DIM}}{\typeout{IS NUMBER}}
\IfIsDim{ .5 }{\typeout{IS DIM}}{\typeout{IS NUMBER}}
\IfIsDim{3.5 in}{\typeout{IS DIM}}{\typeout{IS NUMBER}}
\IfIsDim{3.5pq}{\typeout{IS DIM}}{\typeout{IS NUMBER}}

\stop

Here's the output on the terminal

IS DIM
IS NUMBER
IS NUMBER
IS NUMBER
IS NUMBER
IS DIM
! Undefined control sequence.
<argument> \ERROR

l.41 ...pq}{\typeout{IS DIM}}{\typeout{IS NUMBER}}

?
egreg
  • 1,121,712
  • The l3regex package mentioned in the answer should no longer be loaded, because it has been incorporated in expl3. – egreg Nov 13 '19 at 12:36
8

Here is the way how PSTricks sets a length with or without a unit (run with tex):

\catcode`\@=11
\newdimen\psunit \psunit=10pt% the current unit, can be any value
\def\pstunit@off{\let\@psunit\ignorespaces\ignorespaces}
%
\def\pssetlength#1#2{%  #1: dimen  #2 value (unit)
  \let\@psunit\psunit
  \afterassignment\pstunit@off
  #1 #2\@psunit}

\pssetlength\psunit{2} \the\psunit   %  2 times of the current unit

\pssetlength\psunit{1cm} \the\psunit %  absolute 1cm, the new current unit and so on
\bye

It is really simple:

  • If #2 has no unit then the dimen #1 is set to #2\@psunit and the \afterassignment has no meaning because it is executed after the length setting.
  • If #2 has a unit then the dimen #1 is set to #2 and the following \@psunit is like a \ignorespaces; it was redefined by \afterassignment.
4

Thanks to @MarkWibrow, I've been pointed in the correct direction.

Between Mark Wibrow's answer and the solution posted by @DavidCarlisle regarding plain TeX theory, \afterassignment, I've been able to put together an answer which avoids having to load the entire pgfmath library.

Here's what I came up with:

\documentclass{article}
\makeatletter
\newif\ifaemath@dimen@
\newlength{\ae@dummy@length}
\def\aemath@dimen@#1{%
  \begingroup
    \afterassignment\aemath@dimen@@%
    \ae@dummy@length=#1pt\relax\aemath@}
\def\aemath@dimen@@#1#2\aemath@{%
  \endgroup%%
  \ifx#1\relax%
    \aemath@dimen@false
  \else
    \aemath@dimen@true
  \fi}
\def\ifIsDimension#1#2#3{%%
  \aemath@dimen@{#1}%%
  \ifaemath@dimen@ #2\else #3\fi}
\makeatother
\begin{document}

Hello

\ifIsDimension{2cm}{Dimension}{Other}

\ifIsDimension{0.25}{Dimension}{Other}

\end{document}

enter image description here

Essentially, I looked at the code for \ifpgfmathunitsdeclared and found that the core of what makes this works lies in the code for the macro \pgfmath@dimen@. After getting a better understanding of what \afterassignment does (from David Carlisle's answer mentioned above), I was able to tweak the pgfmath code get something working.

A.Ellett
  • 50,533
  • A few comments on your implementation: In \aemath@dimen@@ it is safer to do \ifx\relax#1%, as the only thing failing this test would be if #1=\relax..., in your version anything would fail with #1=<tok><tok>... (so anything starting with two identical tokens). Also you don't need \ifaemath@dimen@, you could as well use \long\def\aemath@dimen@@#1\aemath@#2#3{\endgroup\ifx\relax#1#3\else#2\fi} and use \let\ifIsDimension\aemath@dimen@ instead of your definition of \ifIsDimension. – Skillmon Nov 09 '19 at 12:42
4

I used A. Ellet's answer, but shortened it to

\def\isdimen#1{\afterassignment\isdimen@\tempdimen=#1em\relax}
\def\isdimen@#1\relax{\ifx&#1&\expandafter\@secondoftwo\else \expandafter\@firstoftwo\fi}

Also, you can use the same idea to check whether something is an integer:

\def\isint#1{\afterassignment\isint@\tempcount=#1\relax}
\def\isint@#1\relax{\ifx&#1&\expandafter\@firstoftwo\else \expandafter\@secondoftwo\fi}

Sadly, these aren't expandable.

Also if you want to use the \pgfmathparse route to check whether something is an integer, the following expandable macro checks whether the value of \pgfmathresult is either of the form n or n.0.

\def\ifpgfmathresultisint{\expandafter\ifpgfmathresultisint@\pgfmathresult..\nil}
\def\ifpgfmathresultisint@#1.#2.#3\nil{%
    \ifx\nil#2\nil
        \expandafter\@firstoftwo
    \else
        \ifnum#2=0
            \expandafter\expandafter\expandafter\@firstoftwo
        \else
            \expandafter\expandafter\expandafter\@secondoftwo
        \fi
    \fi
}
egreg
  • 1,121,712
Hood Chatham
  • 5,467
  • you have an extra space token after & in \isdimen@ and \isint@. –  Jan 01 '16 at 21:15
  • I fixed another spurious space; I'd see better \ifx\hfuzz#2\hfuzz rather than \nil. – egreg Jan 01 '16 at 21:48
  • That has the advantage in general that \hfuzz has some other definition, so \ifx\hfuzz\undefinedcommand\hfuzz gives false (doesn't matter in this case though). I usually use \someprefix@nil, and then \def\myprefix@nil{unique expansion text}. It's a nasty surprise when the argument starts with an undefined command and the test passes. – Hood Chatham Jan 02 '16 at 02:28
3

As expandability was mentioned in this answer, here is one such expandable test. Read the code comments about what it can or not treat.

\documentclass{article}

% Expandable macro to check if some input #1 restricted
% to be composed with
%  signs, optional digits, optional decimal mark, optional digits, one
%  optional ending unit of dimension
% There must be at least one digit.
% 
% returns
%  \z@  if an integer (< 2^30)
%  \@ne if a decimal number (=dimension without unit)
%  \tw@ if a dimension

% badly formed inputs (wrong units etc...) are NOT treated.

% Non explicit things like \ht0 or \count0 do pass through.

% Inputs such as +.5 are annoying as we can't feed them to \numexpr.
% Hence I use \dimexpr, but integers are then limited to be <2^30 rather than
% <2^31.

\makeatletter
\def\checktype #1% returns \z@, \@ne, or \tw@.
   {\romannumeral`\^^@\expandafter\checktype@i\the\numexpr0*\dimexpr#1sp!?{#1}}%
\def\checktype@i 0#1#2?%
   {\ifx!#1\expandafter\checktype@ii\else\expandafter \checktype@isdim\fi}%
\def\checktype@isdim #1{\tw@}%
\def\checktype@ii #1% integer or decimal
   {\expandafter\checktype@iii #1.?!}%
\def\checktype@iii #1.#2#3!%
   {\ifx?#2\expandafter\z@\else\expandafter\@ne\fi}%

\def\TellType #1{\ifcase\checktype{#1}%<- leave no space here
    \texttt{\detokenize{#1}} is \emph{INTEGER}\or 
    \texttt{\detokenize{#1}} is \emph{DECIMAL}\or
    \texttt{\detokenize{#1}} is \emph{DIMENSION}\fi }

\begin{document}\thispagestyle{empty}
\Large

\TellType{3.5pt}\par
\TellType{3.5}\par
\TellType{-3}\par
\TellType{+--.5}\par
\TellType{ .5 }\par
\TellType{3.5 in}\par
% \TellType{3.5pq} % bad input is not treated
\TellType{-+-++---1278997}\par
\TellType{-+-++---1278.997}\par
\TellType{-+-++---1278.997bp}\par
\TellType{-+-++---1278997.}\par
\TellType{-+-++---.1278997}\par
\TellType{-+-++---.1278997ex}\par
\TellType{\number"3FFFFFFF}\par
% \TellType{\number"40000000}\par % "Dimension too large." error
\TellType{\ht0}\par
\TellType{\count0}\par
\end{document}

output

Blockquote

2

The macros provided here only test whether something is an integer/float or a dimension. Arguments which are neither are not treated and might fail badly.

I did try some things and the fastest I came up with as of now is the following (non-expandable):

\newdimen\myifdimen@gobbledimen
\newdimen\myifdimen@end
% if #1 is no dimen, but a number/float it'll be used as a factor of
% \myifdimen@end, resulting in the first \myifdimen@end being removed from the
% input stream as part of the assignment. After the assignment everything till
% the first \myifdimen@end will be gobbled, leaving just \@secondoftwo in the
% number/float case, and \myifdimen@true otherwise.
\protected\long\def\myifdimen#1%
  {%
    \afterassignment\myifdimen@
    % don't remove the space v
    \myifdimen@gobbledimen=#1 \myifdimen@end\myifdimen@true
    \myifdimen@end\@secondoftwo
  }
\long\def\myifdimen@#1\myifdimen@end{}
% just removes the remaining tokens of the test and uses the first of two
% arguments
\long\def\myifdimen@true\myifdimen@end\@secondoftwo#1#2{#1}

The fastest expandable version I got so far is:

\let\myifdimenExp@stop\relax
% \dimexpr will be ended by \myifdimen@end if #1 is a complete dimension. If
% that's not the case \myifdimen@end will be part of the expression, and
% \dimexpr will remove the first \myifdimenExp@stop, as \dimexpr is ended by
% \relax and absorbs it. \myifdimenExp@ will remove anything up to the first
% \myifdimenExp@stop, so if #1 was a dimension \myifdimenExp@true will be used,
% else \@secondoftwo.
\long\def\myifdimenExp#1%
  {%
    % don't remove the space                v
    \expandafter\myifdimenExp@\the\dimexpr#1 \myifdimen@end\myifdimenExp@stop
    \myifdimenExp@true\myifdimenExp@stop\@secondoftwo
  }
% gobble the result of \dimexpr, and possibly \myifdimenExp@true.
\long\def\myifdimenExp@#1\myifdimenExp@stop{}
% removes the remainder of the test and uses the first argument.
\long\def\myifdimenExp@true\myifdimenExp@stop\@secondoftwo#1#2{#1}

Both versions work for integers and floats <2^31 and dimensions up to \maxdimen (usually 16383.99998pt).

The expandable version needs about 30% more time than the non-expandable one.

A complete document using both versions:

\documentclass[]{article}

\makeatletter \newdimen\myifdimen@gobbledimen \newdimen\myifdimen@end

\protected\long\def\myifdimen#1% {% \afterassignment\myifdimen@ \myifdimen@gobbledimen=#1 \myifdimen@end\myifdimen@true \myifdimen@end@secondoftwo } \long\def\myifdimen@#1\myifdimen@end{} \long\def\myifdimen@true\myifdimen@end@secondoftwo#1#2{#1}

\let\myifdimenExp@stop\relax \long\def\myifdimenExp#1% {% \expandafter\myifdimenExp@\the\dimexpr#1 \myifdimen@end\myifdimenExp@stop \myifdimenExp@true\myifdimenExp@stop@secondoftwo } \long\def\myifdimenExp@true\myifdimenExp@stop@secondoftwo#1#2{#1} \long\def\myifdimenExp@#1\myifdimenExp@stop{}

\makeatother

\begin{document} \myifdimen{1}{yes}{no} \myifdimen{1pt}{yes}{no}

\edef\foo{\myifdimenExp{1}{yes}{no} \myifdimenExp{1pt}{yes}{no}} \texttt{\meaning\foo} \end{document}

Skillmon
  • 60,462