The code inside siunitx
(v2) uses a loop over each token in the input before comparing this to the various lists of possible 'number parts'. As that process cannot make assumptions about the nature of the tokens and as it covers a variety of input forms, it's rather complex. I'm currently re-writing it all to be more efficient and clear but the need to work with a range of inputs means its still non-trivial.
With the more restricted input form given here a number of concepts can be hard-coded to produce a much faster parser. For example, the following uses a simple token-by-token loop over the input with the knowledge that the first token can be a +
or -
, other tokens can be 01234567890
and that exactly one .
or ,
is allowed to break between the integer and decimal parts.
\catcode`\@=11 %
\def\num#1{%
\begingroup
\edef\num@temp{#1}%
\def\num@int@part{}%
\def\num@dec@part{}%
\ifx\num@temp\empty
\else
\expandafter\num@first\num@temp\num@stop
\fi
\ifx\num@int@part\empty
\ifx\num@dec@part\empty
\ERROR
\fi
\fi
$
\num@sign@part
\ifx\num@int@part\empty
0%
\else
\num@int@part
\fi
\ifx\num@dec@part\empty
\else
.\num@dec@part
\fi
$%
\endgroup
}
\def\num@first#1#2\num@stop{%
\ifnum
\ifx#1+ 1\fi
\ifx#1- 1\fi
0>0 %
\def\num@sign@part{#1}%
\expandafter\num@first@auxi
\else
\def\num@sign@part{}%
\expandafter\num@first@auxii
\fi
{#1}{#2}%
}
\def\num@first@auxi#1#2{\num@int#2\num@stop}
\def\num@first@auxii#1#2{\num@int#1#2\num@stop}
\def\num@int#1{%
\ifx\num@stop#1%
\else
\ifnum
\ifx#1. 1\fi
\ifx#1, 1\fi
0>0 %
\expandafter\expandafter\expandafter\num@dec
\else
\num@digit@check#1\num@int@part
\expandafter\expandafter\expandafter\num@int
\fi
\fi
}
\def\num@dec#1{%
\ifx\num@stop#1%
\else
\num@digit@check#1\num@dec@part
\expandafter\num@dec
\fi
}
\def\num@digit@check#1#2{%
\begingroup
\def\num@digit@check@aux##1#1{}%
\toks0\expandafter{\num@digit@check@aux0123456789{}{}#1}%
\edef\num@digit@check@aux{\the\toks0}%
\expandafter\endgroup
\ifx\num@digit@check@aux\empty
\def#2{}%
\ERROR
\else
\edef#2{#2#1}%
\fi
}
\def\num@stop{\num@stop}
\long\def\gobble#1{}
\num{1234.56789010}
\bye
I've used a simple error trap: an undefined control sequence. A real version would of course need to be a bit more verbose here.
Spaces in arguments are awkward. In siunitx
I simply ignore them, which is easy as a normal token-based mapping will do it at the TeX level. If you really want to check for spaces in such a case it is doable but more tedious and I'm not sure worth the effort. (Usually the input should be something 'reasonable'.)
In my revisions for siunitx
I'm going with a path a bit more like Steven B. Segletes's answer. For me, that makes sense as I can divide up parts with different restrictions and only need a subset of tests. With the simpler number here such division is probably not much of a saving so I've gone with a single mapping.
An alternative approach to checking the digits is to use TeX, which limits us to the range for count registers but does avoid needing a loop over everything. That might for example read
\catcode`\@=11 %
\def\num#1{%
\begingroup
\edef\num@temp{#1}%
\def\num@int@part{}%
\def\num@dec@part{}%
\ifx\num@temp\empty
\else
\expandafter\num@first\num@temp\num@stop
\fi
\ifx\num@int@part\empty
\ifx\num@dec@part\empty
\ERROR
\fi
\fi
$
\num@sign@part
\ifx\num@int@part\empty
0%
\else
\num@int@part
\fi
\ifx\num@dec@part\empty
\else
.\num@dec@part
\fi
$%
\endgroup
}
\def\num@first#1#2\num@stop{%
\ifnum
\ifx#1+ 1\fi
\ifx#1- 1\fi
0>0 %
\def\num@sign@part{#1}%
\expandafter\num@first@auxi
\else
\def\num@sign@part{}%
\expandafter\num@first@auxii
\fi
{#1}{#2}%
}
\def\num@first@auxi#1#2{\num@int{#2}}
\def\num@first@auxii#1#2{\num@int{#1#2}}
\def\num@int#1{%
\ifx\relax#1\relax
\expandafter\gobble
\else
\expandafter\num@int@auxi
\fi
{#1}%
}
\def\num@int@auxi#1{\num@int@auxii#1\num@stop}
\def\num@int@auxii#1#2\num@stop{%
\ifnum
\ifx#1. 1\fi
\ifx#1, 1\fi
0>0 %
\num@dec{#1#2}%
\else
\num@digit@check{#1#2}\num@int@part\num@dec
\fi
}
\def\num@dec#1{%
\ifx\relax#1\relax
\else
\num@dec@auxi#1\num@stop
\fi
}
\def\num@dec@auxi#1#2\num@stop{%
\num@dec@auxii{#2}%
}
\def\num@dec@auxii#1{%
\ifx\relax#1\relax
\else
\num@dec@auxiii#1\num@stop
\fi
}
\def\num@dec@auxiii#1#2\num@stop{%
\ifnum
\ifx#1+ 1\fi
\ifx#1- 1\fi
0>0 %
\ERROR
\else
\num@digit@check{#1#2}\num@dec@part\num@end
\fi
}
\def\num@end#1{%
\ifx\relax#1\relax\else\ERROR\fi
}
\def\num@digit@check#1#2#3{%
\begingroup
\def\num@digit@check@aux##1\relax{%
\edef\num@digit@check@aux{%
\def\noexpand#2{\the\count0 }%
\noexpand#3{##1}%
}%
}%
\afterassignment\num@digit@check@aux\count0=#1\relax
\expandafter\endgroup
\num@digit@check@aux
}
\def\num@stop{\num@stop}
\long\def\gobble#1{}
\num{1234.56789010}
\bye
Again, I've not forbidden every possible space: doable but tedious and I'd rather not!
\relax+99.123
would typeset +99.123 but presumably that isn't an answer?+
,-
,.
,,
, and switch the appropiate flags and then execute a generic macro.