Checking if argument is a floating point without breaking on control sequences in argument

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


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:

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\number0#1}}}
\def\Test@after@integer#1{%
\expandafter\Test@ifempty\expandafter{%
\Test@ifempty{#2}{}%
\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. • Perhaps \s for a space. Didn't test yours. – egreg Jun 15 at 16:38 • @egreg Thanks, fixed. I also forgot to mention that the l3regex version was stolen from the documentation ;-) – Phelype Oleinik Jun 15 at 16:47 • +1 for the test cases:-) – David Carlisle Jun 15 at 19:57 • @DavidCarlisle You're welcome :-) – Phelype Oleinik Jun 15 at 19:59 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 \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}

• Up until today I didn't know pdfTeX had regex support. Really nice :-) – Phelype Oleinik Jun 15 at 19:56