# Extract the numerical and non-numerical portion from text

I would like to extract the leading number and subsequent text from a string. I have an idea of using the xstring package to gobble characters from the right until I end up with a number or an empty string, but wondering if there is an easier way to do it. The leading number is all the text up until the first character that is not a digit, period, or a plus and minus sign.

Don't really need to worry about error cases such as:

• an additional +, - within the number
• more digits after first non-numerical digit

So, I should get the following output with the definitions of \ExtractLeadingNumber and \ExtractTralingNonDigits completed:

## Code:

\documentclass[border=2pt]{standalone}
\usepackage{booktabs}

\newcommand*{\ExtractTralingNonDigits}[1]{#1}%

% ignore #2 and #3 as those are only needed to produce the desired output
%\newcommand{\Test}[3]{#1&#2&#3\\}% This produces desired output

\begin{document}
\begin{tabular}{l r r r}
& &Number &Non-Digits\\

\midrule
Decimal:
&\Test{ 1.01abc}{ 1.01}{abc}
&\Test{+2.01abc}{+2.01}{abc}
&\Test{-3.01abc}{-3.01}{abc}

\midrule
Integer:
&\Test{  abc}{  }{abc}
&\Test{ 5abc}{ 5}{abc}
&\Test{+6abc}{+6}{abc}
&\Test{-7abc}{-7}{abc}

\midrule
Floating Point:
&\Test{ 5.34abc}{ 5.34}{abc}
&\Test{+6.34abc}{+6.34}{abc}
&\Test{-7.34abc}{-7.34}{abc}

\midrule
Number Only:
&\Test{3}{3}{}
&\Test{3.2}{3.2}{}
&\Test{-5.1}{-5.1}{}
&\Test{+5.1}{+5.1}{}

\midrule
No Digits:
&\Test{abc}{}{abc}

\midrule
Formatted Text:
&\Test{  8$abc_1$}{  8}{$abc_1$}
&\Test{-8.2$abc_1$}{-8.2}{$abc_1$}
&\Test{+$abc_1$}{+}{$abc_1$}
&\Test{$abc_1$}{}{$abc_1$}% no digits
\end{tabular}
\end{document}

-
Do you want a solution using xstring, or are you aiming for plain tex? –  cmhughes Nov 18 '11 at 6:21
@cmhughes: xstring would be fine –  Peter Grill Nov 18 '11 at 7:13

Here is a solution with xstring:

\documentclass[border=2pt]{standalone}
\usepackage{booktabs}
\usepackage{xstring}
\makeatletter
% first, need to fix a bug in xstring:
\@xs@newmacro\IfDecimal{}{1}{0}{%
\@xs@formatnumber{#1}\@xs@reserved@A
\decimalpart\z@
\afterassignment\@xs@defafterinteger\integerpart\@xs@reserved@A\relax\@xs@nil
\expandafter\@xs@testdot\@xs@afterinteger\@xs@nil
\ifx\@empty\@xs@afterdecimal\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi}

\newcommand*\Test[1]{%
\IfBeginWith{#1}{ }{\StrBehind{#1}{ }[\temp@@]}{\def\temp@@{#1}}%
\IfDecimal\temp@@
{\def\temp@{#1&#1&}}
{\def\temp@{#1&}%
\StrBefore{#1}\@xs@afterdecimal[\temp@@]%
}%
\temp@\\}
\makeatother
\begin{document}
\begin{tabular}{l r r r}
& &Number &Non-Digits\\
\midrule
Decimal:
&\Test{ 1.01abc}
&\Test{+2.01abc}
&\Test{-3.01abc}

\midrule
Integer:
&\Test{  abc}
&\Test{ 5abc}
&\Test{+6abc}
&\Test{-7abc}

\midrule
Floating Point:
&\Test{ 5.34abc}
&\Test{+6.34abc}
&\Test{-7.34abc}

\midrule
Number Only:
&\Test{3}
&\Test{3.2}
&\Test{-5.1}
&\Test{+5.1}

\midrule
No Digits:
&\Test{abc}

\midrule
Formatted Text:
&\Test{  8$abc_1$}
&\Test{-8.2$abc_1$}
&\Test{+$abc_1$}
&\Test{$abc_1$}
\end{tabular}
\end{document}


EDIT: here is how to do with \ExtractLeadingNumber and \ExtractTralingNonDigits

\makeatletter
% first, need to fix a bug in xstring:
\@xs@newmacro\IfDecimal{}{1}{0}{%
\@xs@formatnumber{#1}\@xs@reserved@A
\decimalpart\z@
\afterassignment\@xs@defafterinteger\integerpart\@xs@reserved@A\relax\@xs@nil
\expandafter\@xs@testdot\@xs@afterinteger\@xs@nil
\ifx\@empty\@xs@afterdecimal\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi}

\IfBeginWith{#1}{ }{\StrBehind{#1}{ }[\temp@@]}{\def\temp@@{#1}}%
\IfDecimal\temp@@{#1}{\StrBefore{#1}\@xs@afterdecimal}%
}
\newcommand*\ExtractTralingNonDigits[1]{%
\IfBeginWith{#1}{ }{\StrBehind{#1}{ }[\temp@@]}{\def\temp@@{#1}}%
\IfDecimal\temp@@{}\@xs@afterdecimal
}
\makeatother


-
This works pretty well. But for my application, I need to be able to extract the two parts separately which is why I had set up the \ExtractLeadingNumber, and \ExtractTralingNonDigits in the MWE. And since I have not gotten the hang of the expandafter, I am not able to adapt this code as is. So either there should be two separate commands, or perhaps do something like pgf does with math where you access \pgfmathresult immediately after (i.e., a macro which would use \renewcommand to set define the values of two macros. –  Peter Grill Nov 18 '11 at 20:47
I edited the the macro version so that cut-and-paste yields the complete code required. Also, how do I use this in a computation as in \pgfmathsetmacro{\result}{7 + \ExtractLeadingNumber{123K}}. Can you add some detail about the bug that you were fixing here from xstring. –  Peter Grill Nov 20 '11 at 3:58
You never said that you wanted to use \ExtractLeadingNumber{123K}} in \pgfmathparse (the best would be to explain exactly what you want)! Indded, since \ExtractLeadingNumber{123K}} is not purely expandable, you cannot write it in the argument of \pfgmathparse. You should store the result of \ExtractLeadingNumber{123K}} in a macro (for example \resultextract) and then write \pgfmathsetmacro{\result}{7 + \resultextract}. The "bug" in xstring should have benn fixed a year ago, but I forgot it: a \detokenize had to be removed. –  unbonpetit Nov 20 '11 at 8:24

An approach using the LaTeX3 l3regex module

\documentclass{article}
\usepackage[T1]{fontenc}
\usepackage{array,booktabs,expl3,l3regex}
\ExplSyntaxOn
\tl_new:N \l_extract_tl
\regex_set:Nn \l_extract_tl { ^\s*([+-]?\d*\.?\d*)\s*(.*) }
\seq_new:N \l_extract_seq
\tl_new:N \NumberValue
\tl_new:N \OtherValue
\cs_new_protected:Npn \extract_number:n #1
{
\regex_extract_once:NnN  \l_extract_tl {#1} \l_extract_seq
\tl_gset:Nx \NumberValue { \seq_item:Nn \l_extract_seq { 2 } }
\tl_gset:Nx \OtherValue { \seq_item:Nn \l_extract_seq { 3 } }
}
\cs_new_protected:Npn \Test #1
{
\extract_number:n {#1}
& \detokenize{#1} & \NumberValue & \OtherValue
}
\ExplSyntaxOff
\begin{document}
\begin{tabular}{l>{\ttfamily}r>{\ttfamily}r>{\ttfamily}r}
\toprule
& \multicolumn{1}{r}{Input} &
\multicolumn{1}{r}{Digit} & \multicolumn{1}{r}{Non-digit} \      \midrule
Decimal:  \Test{ 1.01abc}               \                 \Test{+2.01abc}               \\
\Test{-3.01abc}               \      \midrule
Integer:  \Test{  abc}                  \                 \Test{ 5abc}                  \\
\Test{+6abc}                  \                 \Test{-7abc}                  \      \midrule
Floating Point: \Test{ 5.34abc}         \                       \Test{+6.34abc}         \                       \Test{-7.34abc}         \      \midrule
Number Only:    \Test{3}                \                       \Test{3.2}              \\
\Test{-5.1}             \                       \Test{+5.1}             \      \midrule
No Digits:      \Test{abc}              \      \midrule
Formatted Text: \Test{  8$abc_1$}       \\
\Test{-8.2$abc_1$}      \\
\Test{+$abc_1$}         \                       \Test{$abc_1$}          \      \bottomrule
\end{tabular}
\end{document}


Currently, this module is 'experimental' hence loading it separately from expl3, but I'd expect it to move to 'kernel' in the near-ish future (before the end of the year).

The way that this works is that when we do a regular expression match, the capturing groups are stored in a sequence indexed from 0 (the complete match) upward. So I've got the first capture group as the numerical part and the second as the non-numerical. Notice that I've also included \s* to remove any leading spaces from those two groups: if you miss that out then you'll also pick up the spaces as part of the match.

Also notice that the results here are detokenized, so if you want to have formatted text you'd need to \scantokens the results. (Something as simple as \scantokens\expandafter{\OtherValue} would do here.)

-
In the CTAN version, I think we have \regex_extract:NnN rather than \regex_extract_once:NnN. This will change at the weekend, as I'm due a CTAN snapshot. –  Joseph Wright Nov 18 '11 at 9:44
Notice that I've used a 'pre-compiled' regex here as it's used many times and performance can therefore be gained by setting up the regex first. You use \regex_extract_once:nnN with a 'one-use' regex. –  Joseph Wright Nov 18 '11 at 9:45
Seems to work fine once I change \regex_extract_once:NnN to \regex_extract:NnN in the MWE. I guess I am going to have to learn this syntax one of these days. –  Peter Grill Nov 18 '11 at 20:52
@PeterGrill As I said, I'll update CTAN probably tomorrow to deal with that. Of course, someone could convert Bruno's code to non-LaTeX3 syntax (in this case it might not be so bad as a lot of the code is primitives). –  Joseph Wright Nov 18 '11 at 22:04
@Joseph or someone can simply write a LaTeX2e-like wrapper for that module. \def\ExtractOnce{\@ifstar\regex_extract_once:NnN\regex_extract_once:nnN} etc. –  Bruno Le Floch Nov 19 '11 at 4:36

If you can use luatex, you may use a proper parser (the code below is in ConTeXt, just because I don't know all the details of using luatex in LaTeX).

 \startluacode
local P, R, S, V, match = lpeg.P, lpeg.R, lpeg.S, lpeg.V, lpeg.match
local Ct, C, Cs, Cc = lpeg.Ct, lpeg.C, lpeg.Cs, lpeg.Cc

local format = string.format

local digit    = R("09")
local sign     = S('+-')
local integer  = sign^0 * digit^0 -- NOTE: I'd rather use digit^1, but
-- the requirements want to capture a
--  single sign as well
local float    = sign^0 * digit^0 * P('.') * digit^1
local space    = P(" ")^0

local number   = Cs(float + integer)
local any      = Cs(P(1)^0)

local number_value = Cc("\\global\\def\\NumberValue{%s}") * number / format
local other_value  = Cc("\\global\\def\\OtherValue{%s}")  * any    / format
local parser = Cs(space * number_value * other_value)

function commands.extract_number(s)
context(match(parser,s))
end
\stopluacode

\unprotect
\def\extract#1%
{\let\NumberValue\relax
\let\OtherValue \relax
\ctxcommand{extract_number(\!!bs\detokenize{#1}\!!es)}}
\protect


You can then use this as follows.

\def\Test#1%
{\extract{#1}%
#1 \NC \NumberValue \NC \OtherValue}

\starttext

\starttabulate[|l|r|r|r|]
\HL
\NC           \NC Input \NC Digit \NC Non-Digit \NC \NR
\HL
\NC Decimal:  \NC \Test{ 1.01abc}               \NC \NR
\NC           \NC \Test{+2.01abc}               \NC \NR
\NC           \NC \Test{-3.01abc}               \NC \NR
\HL
\NC Integer:  \NC \Test{  abc}                  \NC \NR
\NC           \NC \Test{ 5abc}                  \NC \NR
\NC           \NC \Test{+6abc}                  \NC \NR
\NC           \NC \Test{-7abc}                  \NC \NR
\HL
\NC Floating Point: \NC \Test{ 5.34abc}         \NC \NR
\NC                 \NC \Test{+6.34abc}         \NC \NR
\NC                 \NC \Test{-7.34abc}         \NC \NR
\HL
\NC Number Only:    \NC \Test{3}                \NC \NR
\NC                 \NC \Test{3.2}              \NC \NR
\NC                 \NC \Test{-5.1}             \NC \NR
\NC                 \NC \Test{+5.1}             \NC \NR
\HL
\NC No Digits:      \NC \Test{abc}              \NC \NR
\HL
\NC Formatted Text: \NC \Test{  8$abc_1$}       \NC \NR
\NC                 \NC \Test{-8.2$abc_1$}      \NC \NR
\NC                 \NC \Test{+$abc_1$}         \NC \NR
\NC                 \NC \Test{$abc_1$}          \NC \NR
\HL
\stoptabulate
\stoptext


which gives

-

I can show the plain TeX solution of this problem for the sake of completeness.

\def\separeparts#1{\def\firstpart{}\def\listchars{0123456789.}\separepartsA#1\end}
\def\separepartsA#1{\isinlist{+-}#1%
\iftrue
\def\firstpart{#1}\expandafter\separepartsB
\else
\def\next{\separepartsB#1}\expandafter\next
\fi
}
\def\separepartsB#1{\isinlist\listchars#1%
\iftrue
\ifx.#1\def\listchars{0123456789}\fi
\expandafter\separepartsB
\else
\def\next{\separepartsC#1}\expandafter\next
\fi
}
\def\separepartsC#1\end{\def\secondpart{#1}}


Note that the list of allowed digits \listchars includes decimal point but if decimal point is found then \listchars is redefined because the second decimal point isn't allowed.

This code needn't any package, only two macros from opmac.tex is used. You can copy and paste these macros from opmac.tex or from here:

\def\isinlist#1#2#3{\def\tmp##1#2##2\end{\def\tmp{##2}%
\ifx\tmp\empty \csname iffalse\expandafter\endcsname \else
\csname iftrue\expandafter\endcsname \fi}% end of \def\tmp
\expandafter\tmp#1\endlistsep#2\end
}


Now, you can do tests:

\def\test#1{\separeparts{#1}
\immediate\write16{"#1" = "\firstpart" and "\secondpart"}
}

\test{+2.01abc}   % output: "+2.01abc" = "+2.01" and "abc"
\test{-3.01abc}   % output: "-3.01abc" = "-3.01" and "abc"

\test{  abc}      % output: " abc" = "" and "abc"
\test{ 5abc}      % output: " 5abc" = "5" and "abc"
\test{+6abc}      % output: "+6abc" = "+6" and "abc"

\test{1.23.36abc} % output: "1.23.36abc" = "1.23" and ".36abc"


You can use this code in plain TeX, in LaTeX or in ConTeXt. It doesn't matter. The code is based only on TeX primitives.

-