# Parse dotted string, extracting last part

I want a macro that will separate a string like "1.2.3" into two parts: the part until the last dot (call that the context) and the last number. This seems a pretty basic application of a helper macro and \expandafter, but I can't get it to terminate.

Here is my MWE, with some debugging macros thrown in:

\documentclass{article}

\makeatletter
\newcommand{\parse}[1]{%
Parsing #1:\par%
\let\my@context\@empty%
\let\my@number\@empty%
\expandafter\@parse#1.\@nil
Context: \my@context\par
Number: \my@number
}
\def\@parse#1.#2\@nil{%
\if#2\@empty%
\edef\my@number{#1}%
\show\my@number%
\else%
\edef\my@context{\if\my@context\@empty\else\my@context.\fi#1}%
\show\my@context%
\expandafter\@parse#2\@nil%
\fi
}
\makeatother

\begin{document}
\tracingmacros=1
\parse{1.2.3}

\parse{4.5.6.7}

\parse{1.2}
\tracingmacros=0

\end{document}


As I compile this I get to what ought to be the terminating case but it doesn't succeed. Here is the log file:

\parse #1->Parsing #1:\par \let \my@context \@empty \let \my@number \@empty \ex
pandafter \@parse #1.\@nil Context: \my@context \par Number: \my@number
#1<-1.2.3

\@parse #1.#2\@nil ->\if #2\@empty \edef \my@number {#1}\show \my@number \else
\edef \my@context {\if \my@context \@empty \else \my@context .\fi #1}\show \my@
context \expandafter \@parse #2\@nil \fi
#1<-1
#2<-2.3.

\my@context ->

\@empty ->

> \my@context=macro:
->1.
\@parse ... \my@context .\fi #1}\show \my@context
\expandafter \@parse #2\@n...
l.30 \parse{1.2.3}

?

\@parse #1.#2\@nil ->\if #2\@empty \edef \my@number {#1}\show \my@number \else
\edef \my@context {\if \my@context \@empty \else \my@context .\fi #1}\show \my@
context \expandafter \@parse #2\@nil \fi
#1<-2
#2<-3.

\my@context ->1

\@empty ->

\my@context ->1
> \my@context=macro:
->1.2.
\@parse ... \my@context .\fi #1}\show \my@context
\expandafter \@parse #2\@n...
l.30 \parse{1.2.3}

?

\@parse #1.#2\@nil ->\if #2\@empty \edef \my@number {#1}\show \my@number \else
\edef \my@context {\if \my@context \@empty \else \my@context .\fi #1}\show \my@
context \expandafter \@parse #2\@nil \fi
#1<-3
#2<-

\@empty ->

\my@number ->

\my@context ->1.2

\my@context ->1.2
> \my@context=macro:
->1.2.3.
\@parse ... \my@context .\fi #1}\show \my@context
\expandafter \@parse #2\@n...
l.30 \parse{1.2.3}

?
! Undefined control sequence.
\@parse ...y@context \expandafter \@parse #2\@nil
\fi
l.30 \parse{1.2.3}

?


In the last successful expansion of \@parse, #1 is "3" and #2 is "". It seems that \@empty expands to "" as well. So why does \if#2\empty not succeed?

I welcome fancy xparse and l3regex solutions but would love a basic one as well. There's something simple I'm not getting.

-
you probably meant ifx not if. \if#2\empty  expands tokens until it finds the first two non expandable tokens and then compares them. so if #2 is aa it would be true if #2 is ab it would be false and if #2 is a it depends what comes next and if #2 is empty it depends on the next two things –  David Carlisle Nov 29 '12 at 14:29

The problem with your code as-is is the line:

\if#2\@empty


Suppose that the second parameter is empty. When TeX substitutes in the parameter text then it inserts the second parameter in place of #2, so #2 is not a macro that expands to the parameter text. It is the parameter text. Thus the stream becomes \if\empty and TeX tries to find the next thing to compare \empty with (in this case, \edef whence the test fails).

One way to fix this with minimal change to your code is to store the contents of #2 in a macro and then test that macro. As we're now testing macros, \ifx is the right choice. So now you need:

\def\arg@two{#2}%
\ifx\arg@two\@empty


This will test to see if #2 is empty or not, as you want.

In full:

\documentclass{article}

\makeatletter
\newcommand{\parse}[1]{%
Parsing #1:\par%
\let\my@context\@empty%
\let\my@number\@empty%
\expandafter\@parse#1.\@nil
Context: \my@context\par
Number: \my@number
}
\def\@parse#1.#2\@nil{%
\def\arg@two{#2}%
\ifx\arg@two\@empty%
\edef\my@number{#1}%
\show\my@number%
\else%
\edef\my@context{\if\my@context\@empty\else\my@context.\fi#1}%
\show\my@context%
\expandafter\@parse#2\@nil%
\fi
}
\makeatother

\begin{document}
\tracingmacros=1
\parse{1.2.3}

\parse{4.5.6.7}

\parse{1.2}
\tracingmacros=0

\end{document}


Produces:

-
(upvoted) Thanks. Explaining the difference between "the parameter text" and a "macro that expands to the parameter text" helped a lot! –  Matthew Leingang Nov 29 '12 at 14:51
An emptyness test for #2 might be the usual \if\relax\detokenize{#2}\relax –  egreg Nov 29 '12 at 14:58

After the wiz have spoken, a pragmatic solution :

\documentclass{article}
\usepackage{xstring}
\newcommand{\parse}[1]{
Parsing #1:\par%
\StrCount{#1}{.}[\dotnum]
Context : \StrBefore[\dotnum]{#1}{.}\par
Number:  \StrBehind[\dotnum]{#1}{.}
}
\begin{document}

\parse{1.2.3}

\parse{4.5.6.7}

\parse{1.2}
\end{document}


-
Thank you for expanding your comment to an answer. Am I right that the \StrCount line counts the number of dots in #1 and stores that in \dotnum? Then the \StrBefore line extracts the part of the string before the \dotnumth dot? –  Matthew Leingang Nov 29 '12 at 15:56
@MatthewLeingang Spot on. –  percusse Nov 29 '12 at 16:06

A plain TeX solution:

\def\parse#1{%
\def\a{}%
\def\b{}%
\xparse#1.\relax.}

\def\xparse#1.#2.{%
\ifx\relax#2
\def\b{#1}%
\expandafter\parsestop
\else
\edef\a{\a\ifx\a\empty\else.\fi#1}%
\expandafter\xparse
\fi
#2.}

\def\parsestop\relax.{%
\immediate\write20{[a=\a] [b=\b]}}

\parse{1}

\parse{1.2.3}

\parse{4.5.6.7}

\parse{1.2}

\bye


which produces

[a=] [b=1]
[a=1.2] [b=3]
[a=4.5.6] [b=7]
[a=1] [b=2]
)
No pages of output.

-
\documentclass{article}

\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\parse}{m}
{
\leingang_parse:n { #1 }
Context~is~\tl_use:N \l_leingang_context_tl;~suffix~is~\tl_use:N \l_leingang_suffix_tl
}

\tl_new:N \l_leingang_context_tl
\tl_new:N \l_leingang_suffix_tl
\seq_new:N \l__leingang_temp_seq

\cs_new_protected:Npn \leingang_parse:n #1
{
\seq_set_split:Nnn \l__leingang_temp_seq { . } { #1 }
\seq_pop_right:NN \l__leingang_temp_seq \l_leingang_suffix_tl
\tl_set:Nx \l_leingang_context_tl
{
\seq_use:Nnnn \l__leingang_temp_seq { . } { . } { . }
}
}
\ExplSyntaxOff

\begin{document}
\parse{1.2.3}

\parse{4.5.6.7}

\parse{1.2}

\end{document}


The argument is split into components at the periods; the last component is detached and put in \l_leingang_suffix_tl for further usage; from what remains the token list \l_leingang_context_tl is formed, reinserting the periods.

-
Thank you for the LaTeX3 version. I look forward to learning the L3 syntax at some point. –  Matthew Leingang Nov 30 '12 at 14:42
@MatthewLeingang Beware that there's a limitation with the current code: spaces around the periods will disappear. But it's not so difficult to maintain them, if needed. –  egreg Nov 30 '12 at 14:46

Here is another variant of David Carlisle's solution. In this solution, the command \parsestop isn't necessary and \a is scoped. Also, we watch out for null/empty tokens.

\documentclass{article}
\def\parse#1{\begingroup\def\a{}\xparse#1.\relax.}
\def\xparse#1.#2.{%
\ifx\relax#2%
\edef\xparse\relax.{%
\endgroup\ifx\a\empty\else[a=\a]\space\fi\ifx\\#1\\\else[b=#1]\fi
}%
\else
\ifx\\#1\\\else
\edef\a{\a\ifx\a\empty\else.\fi#1}%
\fi
\fi
\xparse#2.%
}

\begin{document}
\parse{}

\parse{1}

\parse{1.2.3}

\parse{1..2..3}

\parse{4.5.6.7}

\parse{1.2}
\end{document}

-