Take the 2-minute tour ×
TeX - LaTeX Stack Exchange is a question and answer site for users of TeX, LaTeX, ConTeXt, and related typesetting systems. It's 100% free, no registration required.

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.

share|improve this question
1  
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

5 Answers 5

up vote 9 down vote accepted

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:

Parsed strings

share|improve this answer
    
(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}

enter image description here

share|improve this answer
    
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.
share|improve this answer
\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}

enter image description here

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.

share|improve this answer
    
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} 
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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