5

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
6

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.

  • 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
5

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}
  • Up until today I didn't know pdfTeX had regex support. Really nice :-) – Phelype Oleinik Jun 15 at 19:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.