6

Inspired by this answer to test whether an argument is a positive integer, I would like to extend this to floating point numbers. I want to have a macro \TestNumber that checks, whether its first argument is a floating point or integer and then conditionally outputs either its second or third argument. The twist is, that it should not break when called like \TestNumber{\textbf{Hello}}, which is the case with for example \IfDecimal from the xstring package.

The following does the trick for positive integers (\TestNumber{\textbf{123}}{Number}{Not a number} prints Not a number), but as said, I would like to extend this to floating point numbers as well. So, \TestNumber{12.3}{Number}{Not a number} should output Number, while \TestNumber{\textbf{12.3}}{Number}{Not a number} should output Not a number.

\makeatletter
\def\TestNumber#1{%
  \afterassignment\get@args\count@=0#1\hfuzz#1\hfuzz}
\def\get@args#1\hfuzz#2\hfuzz{%
  \if\relax\detokenize{#1}\relax
    \expandafter\@firstoftwo%
  \else
   \expandafter\@secondoftwo%
  \fi
}
\makeatother
jessepeng
  • 185

2 Answers2

7

The code needs a little more token handling than the simpler case for integers. I made the function expandable, so this adds a little bit more code too, but it's not that much.

First the function uses \detokenize to ensure that TeX doesn't try to expand anything weird later on. After that the code proceeds removing a possible integer part with a possible sign using \romannumeral-0#1 (and some other things), then the code removes a possible decimal separator, and removes the trailing decimal part. After that the code checks whether the remaining token list is empty. If it is, then the argument was a valid number, otherwise it was not.

Testing a few possibilities, returns the desired output:

enter image description here

The code being expandable means that you can do:

\def\TestIfIsAPositiveNumber#1{%
  \ifdim
    \TestNumber{#1}{#1}{-1}pt
      > 0pt
    Positive :)
  \else
    Negative or weird :(
  \fi
}

and get the expected output.

Here's the code:

\documentclass{article}
\makeatletter
\def\TestNumber#1{%
  \Test@ifempty{#1}%
    {\@secondoftwo}%
    {\expandafter\Test@integer\expandafter{\detokenize{#1}}}}%
\def\Test@integer#1{%
  \expandafter\Test@after@integer\expandafter{%
    \romannumeral-0\expandafter\Test@remove@leading@minus\expandafter{%
      \romannumeral-0\number0#1}}}
\def\Test@after@integer#1{%
  \expandafter\Test@ifempty\expandafter{%
    \romannumeral-0\Test@remove@leading@dot{#1}}}
\def\Test@remove@leading@minus#1{%
  \Test@remove@leading-{#1}}
\def\Test@remove@leading@dot#1{%
  \Test@remove@leading.{#1}}
\def\Test@remove@leading#1#2{%
  \Test@ifempty{#2}{}%
    {\Test@@remove@leading#1#2\qstop}}
\def\Test@@remove@leading#1#2#3\qstop{%
  \if\noexpand#1\noexpand#2%
    \expandafter\@firstoftwo
  \else
    \expandafter\@secondoftwo
  \fi{#3}{#2#3}}
\def\Test@ifempty#1{%
  \if\relax\detokenize{#1}\relax
    \expandafter\@firstoftwo
  \else
   \expandafter\@secondoftwo
  \fi}
\makeatother
\begin{document}
\def\test#1{\texttt{\detokenize{#1} = }\TestNumber{#1}{Number}{Not a number}\par}
\test{0}
\test{1}
\test{-1}
\test{.23}
\test{-.23}
\test{1.23}
\test{-1.23}
\test{\textbf{1.23}}
\end{document}

Or, a simpler (but unexpandable) version with l3regex (the expression was copied from interface3 and changed the control spaces \␣ by \s, which matches [\ \^^I\^^J\^^L\^^M], according to the manual):

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\regex_const:Nn \c_jessepeng_float_regex { ^[\+\-\s]*(\d+|\d*\.\d+)\s*$ }
\NewDocumentCommand \TestNumber { m m m }
  { \jessepeng_if_float:nTF {#1} {#2} {#3} }
\prg_new_protected_conditional:Npnn \jessepeng_if_float:n #1 { T, F, TF }
 {
   \regex_match:NnTF \c_jessepeng_float_regex {#1}
     { \prg_return_true: }
     { \prg_return_false: }
 }
\ExplSyntaxOff
\begin{document}
\def\test#1{\texttt{\detokenize{#1} = }\TestNumber{#1}{Number}{Not a number}\par}
\test{0}
\test{1}
\test{-1}
\test{.23}
\test{-.23}
\test{1.23}
\test{-1.23}
\test{\textbf{1.23}}
\end{document}

It yields the same result but is much slower and is not expandable.

6

Borrowing a test file from the other answer, as you failed to supply one in the question, for pdflatex at least you can use its built in regex support

enter image description here

\documentclass{article}

\makeatletter
\def\TestNumber#1{%
\ifnum1=\pdfmatch{^\string\s*-?[0-9]*[.0-9][0-9]*\string\s*$}{\detokenize{#1}}
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi}


\begin{document}
\def\test#1{\texttt{\detokenize{#1} = }\TestNumber{#1}{Number}{Not a number}\par}
\test{}
\test{0}
\test{1}
\test{-1}
\test{.23}
\test{-.23}
\test{1.23}
\test{-1.23}
\test{\textbf{1.23}}
\end{document}

or a version using Lua patterns for luatex

\documentclass{article}

\makeatletter
\ifx\pdfmatch\@undefined
\ifx\directlua\@undefined
% xetex
\typeout{use l3regex from the other answer}
\else
% luatex
\def\TestNumber#1{%
\ifnum1=\directlua{if (string.find("\luaescapestring{\detokenize{#1}}","^[-]?\@percentchar d*[.]?\@percentchar d+$"))
then tex.write("1 ")
else tex.write("0 ")
end
}
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi}
\fi
\else
% pdftex
\def\TestNumber#1{%
\ifnum1=\pdfmatch{^\string\s*-?[0-9]*[.0-9][0-9]*\string\s*$}{\detokenize{#1}}
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi}
\fi
\makeatother


\begin{document}
\def\test#1{\texttt{\detokenize{#1} = }\TestNumber{#1}{Number}{Not a number}\par}
\test{}
\test{0}
\test{1}
\test{-1}
\test{.23}
\test{-.23}
\test{1.23}
\test{-1.23}
\test{\textbf{1.23}}
\end{document}
David Carlisle
  • 757,742