# Comparing reference numbers

I'm using the refcount package to get the reference number associated with a label. I'd like to determine whether two references lay "next" to each other or not, that is: I'd like an expandable command of the form:

\comparerefs{refOne}{refTwo}


that would yield 0 if refOne is equal to refTwo, 1 if refOne is immediately before refTwo, 2 if refOne is immediately after refTwo, and 3 in any other case.

Merely calling \getrefnumber (from the refcount package), saving the result into a counter and adding/subtracting 1 doesn't work for two resons:

1. If refOne refers to a section (and we're using the book class), \getrefnumber return 1.1 and setting the counter breaks.
2. Even if one were to get the section number only, one has to be careful because section 1 of chapter 1 is not immediately before section 2 of chapter 2.

This should obviously work with "deeper" references (ie. references to sub-subsections and the like).

To clarify: assume refOne is of the form a.b.c and refTwo of the form d.e.f, then \comparerefs{refOne}{refTwo} should return 0 if a = d, b = e, and c = f; \comparerefs{refOne}{refTwo} should return 1 if a = d, b = e, and c = f - 1; \comparerefs{refOne}{refTwo} should return 2 if a = d, b = e, and c = f + 1; and \comparerefs{refOne}{refTwo} should return 3 if a != d, or b != e, or c != f - 1, f, f + 1 or if refOne and refTwo are of different lengths.

Ideas?

-
I spent a while on this, but then I checked and saw that you want an expandable solution. Since it is hard even with the nice programming interface of pgfkeys, I'm giving up. Are you sure it has to be expandable? Doing something like \comparerefs{refone}{reftwo}{\result} and then working with the expandable macro \result may be enough unless this is nested deeply in some other expansion-only context. –  Ryan Reich Nov 12 '11 at 21:31
@RyanReich: looking backwards I have to honestly say I don't recall exactly why I wanted it to be expandable :P... I guess it's fine if it's not, better if it is... Care to share your non-expandable solution? :) –  mpr Nov 12 '11 at 21:35

\documentclass[a4paper]{book}
\usepackage{xparse,refcount,etoolbox}
\newcounter{comparerefs}
\newcommand{\comparerefs}[2]{%
\begingroup\edef\x{\endgroup
\noexpand\docomparerefs{\getrefnumber{#1}}{\getrefnumber{#2}}}\x}
\NewDocumentCommand{\docomparerefs}{ >{\SplitArgument{2}{.}}m >{\SplitArgument{2}{.}}m }{
\xcomparerefs#1#2}

\newcommand\xcomparerefs[6]{%
\ifboolexpr{ test {\ifstrequal{#1}{0}} or test {\ifstrequal{#4}{0}} }% reference is undefined
{\setcounter{comparerefs}{-1}}
{\ycomparerefs{#1}{#2}{#3}{#4}{#5}{#6}}%
}
\newcommand{\ycomparerefs}[6]{%
\ifboolexpr{ test {\ifstrequal{#3}{\NoValue}} and test {\ifstrequal{#6}{\NoValue}} }
{\zcomparerefs{#1}{#2}{#4}{#5}}
{\subseccomparerefs{#1}{#2}{#3}{#4}{#5}{#6}}%
}
\newcommand{\zcomparerefs}[4]{%
\ifboolexpr{ test {\ifstrequal{#2}{\NoValue}} and test {\ifstrequal{#4}{\NoValue}} }
{\chapcomparerefs{#1}{#3}}
{\seccomparerefs{#1}{#2}{#3}{#4}}%
}
\newcommand{\chapcomparerefs}[2]{%
\ifnum#1=#2\relax
\setcounter{comparerefs}{0}%
\else
\ifnum#1=\numexpr#2-1\relax\relax
\setcounter{comparerefs}{1}%
\else
\ifnum#1=\numexpr#2+1\relax\relax
\setcounter{comparerefs}{2}%
\else
\setcounter{comparerefs}{3}%
\fi
\fi
\fi
}
\newcommand{\seccomparerefs}[4]{%
\ifnum#1=#3\relax
\chapcomparerefs{#2}{#4}%
\else
\setcounter{comparerefs}{3}%
\fi}
\newcommand{\subseccomparerefs}[6]{%
\ifnum#1=#4\relax
\seccomparerefs{#2}{#3}{#5}{#6}%
\else
\setcounter{comparerefs}{3}%
\fi
}

\begin{document}
\chapter{Abc}\label{x}
\section{Abc}
\subsection{Def}\label{A}
\subsection{Ghi}\label{B}

\section{XXX}
\subsection{UUU}\label{C}

\chapter{YYY}\label{y}

\chapter{ZZZ}\label{z}

%\comparerefs{x}{x}\showthe\value{comparerefs}
%\comparerefs{x}{y}\showthe\value{comparerefs}
%\comparerefs{y}{x}\showthe\value{comparerefs}
%\comparerefs{x}{z}\showthe\value{comparerefs}

\comparerefs{A}{A}\showthe\value{comparerefs}
\comparerefs{A}{B}\showthe\value{comparerefs}
\comparerefs{B}{A}\showthe\value{comparerefs}
\comparerefs{A}{C}\showthe\value{comparerefs}

\end{document}


I use the \SplitArgument feature of xparse to get the references already split in components. I support only up to three levels (subsections) and there is no check whether the references are not compatible.

If one of the references is not already resolved, the counter will be set to -1.

First we decide whether the references are to chapters, sections or subsections. So \chapcomparerefs is the main one and sets the counter to the requested value. In case of \seccomparerefs we check if the first components are equal and then we pass control to \chapcomparerefs on the second components, otherwise we set the counter to 3. Similarly for \subseccomparerefs which sets the counter to 3 if the first components are different, otherwise calls \seccomparerefs.

In case you compare different kinds of references you get a "Missing number" error.

\documentclass[a4paper]{book}
\usepackage{xparse,refcount,etoolbox}
\newcounter{comparerefs}
\newcommand{\comparerefs}[2]{%
\begingroup\edef\x{\endgroup
\noexpand\docomparerefs{\getrefnumber{#1}}{\getrefnumber{#2}}}\x}
\NewDocumentCommand{\docomparerefs}{ >{\SplitArgument{20}{.}}m >{\SplitArgument{20}{.}}m }{
\setcounter{comparerefs}{0}\begingroup
\def\NoValue{-1000}\edef\x{\endgroup
\noexpand\xcomparerefs#1\relax#2\relax}\x}

\def\xcomparerefs#1#2#3\relax#4#5#6\relax{%
\ycomparerefs{#1}{#2}{#4}{#5}{{#2}#3\relax{#5}#6\relax}}
\def\ycomparerefs#1#2#3#4#5{%
\ifboolexpr{ test {\ifnumcomp{#1}{=}{#3}} and
not ( test {\ifnumcomp{#2}{=}{-1000}} and test {\ifnumcomp{#4}{=}{-1000}} ) }
{\xcomparerefs#5}
{\decide{#1}{#2}{#3}{#4}}%
}
\def\decide#1#2#3#4{%
\ifboolexpr{ test {\ifnumcomp{#2}{=}{-1000}} and test {\ifnumcomp{#4}{=}{-1000}} }
{\ifnumcomp{#1}{=}{#3}{}{\xdecide{#1}{#3}}}
{\ifnumcomp{#1}{=}{#3}{\xdecide{#2}{#4}}{\setcounter{comparerefs}{3}}}%
}
\def\xdecide#1#2{%
\ifnumcomp{#1}{=}{#2-1}
{\setcounter{comparerefs}{1}}
{\ifnumcomp{#1}{=}{#2+1}
{\setcounter{comparerefs}{2}}
{\setcounter{comparerefs}{3}}%
}
}

\begin{document}
\chapter{Abc}\label{x}
\section{Abc}
\subsection{Def}\label{A}
\subsection{Ghi}\label{B}

\section{XXX}
\subsection{UUU}\label{C}

\chapter{YYY}\label{y}

\chapter{ZZZ}\label{z}

\comparerefs{x}{x}\showthe\value{comparerefs}
\comparerefs{x}{y}\showthe\value{comparerefs}
\comparerefs{y}{x}\showthe\value{comparerefs}
\comparerefs{x}{z}\showthe\value{comparerefs}

\comparerefs{A}{A}\showthe\value{comparerefs}
\comparerefs{A}{B}\showthe\value{comparerefs}
\comparerefs{B}{A}\showthe\value{comparerefs}
\comparerefs{A}{C}\showthe\value{comparerefs}

\comparerefs{A}{x}\showthe\value{comparerefs}
\end{document}


These macros allow for a maximum of twenty levels (which should be more than sufficient). Again xparse is used to get two lists of arguments from the references. I assume that no reference value is -1000. The result of the test is stored in the counter comparerefs.

This is the expandable version of the second answer (the \edef will testify this assertion). As a byproduct, the length of the strings is unlimited, provided they are all of the stated form, that is, numbers separated by periods. None of the numbers should be -1000, the special value used for terminating the recursion.

\documentclass[a4paper]{book}
\usepackage{refcount,etoolbox}
\newcommand{\comparerefs}[2]{%
\expandafter\comparerefsA\expandafter{\romannumeral-a\getrefnumber{#2}}{#1}}
\def\comparerefsA#1#2{%
\expandafter\comparerefsB\expandafter{\romannumeral-a\getrefnumber{#2}}{#1}}
\def\comparerefsB#1#2{%
\xcomparerefs#1.-1000\relax#2.-1000\relax}

\def\xcomparerefs#1.#2.#3\relax#4.#5.#6\relax{%
\ycomparerefs{#1}{#2}{#4}{#5}{#2.#3.-1000.-1000\relax#5.#6.-1000.-1000\relax}}
\def\ycomparerefs#1#2#3#4#5{%
\ifboolexpe{ test {\ifnumcomp{#1}{=}{#3}} and
not ( test {\ifnumcomp{#2}{=}{-1000}} and test {\ifnumcomp{#4}{=}{-1000}} ) }
{\xcomparerefs#5}
{\decide{#1}{#2}{#3}{#4}}%
}
\def\decide#1#2#3#4{%
\ifboolexpe{ test {\ifnumcomp{#2}{=}{-1000}} and test {\ifnumcomp{#4}{=}{-1000}} }
{\ifnumcomp{#1}{=}{#3}{0}{\xdecide{#1}{#3}}}
{\ifnumcomp{#1}{=}{#3}{\xdecide{#2}{#4}}{3}}%
}
\def\xdecide#1#2{%
\ifnumcomp{#1}{=}{#2-1}
{1}
{\ifnumcomp{#1}{=}{#2+1}
{2}
{3}%
}
}

\begin{document}
\chapter{Abc}\label{x}
\section{Abc}
\subsection{Def}\label{A}
\subsection{Ghi}\label{B}

\section{XXX}
\subsection{UUU}\label{C}

\chapter{YYY}\label{y}

\chapter{ZZZ}\label{z}

\comparerefs{x}{y}\par
\comparerefs{y}{x}\par
\comparerefs{x}{z}\par

\comparerefs{A}{A}\par
\comparerefs{A}{B}\par
\comparerefs{B}{A}\par
\comparerefs{A}{C}\par

\comparerefs{A}{x}\par
\end{document}


## Comments on the "expandable" solution

The \comparerefs macro reads its arguments and first of all tries to get the expansions of \getrefnumber from them. I use the \romannumeral-a trick, but twice: the \expandafter will reach \romannumeral, so from \comparerefs{X}{Y} we get

\comparerefsA{<expansion of \getrefnumber{Y}}{X}


that will be expanded to

\expandafter\comparerefsB\expandafter
{\romannumeral-a\getrefnumber{X}}
{<expansion of \getrefnumber{Y}}


that will become

\comparerefsB{<expansion of \getrefnumber{X}}{<expansion of \getrefnumber{Y}}


Say that \getrefnumber{X} expands to 1.1.1 and \getrefnumber{Y} expands to 1.2 (different length); in this case the successive expansion will be

\xcomparerefs 1.1.1.-1000.-1000\relax 1.2.-1000.-1000\relax


The macro \xcomparerefs just picks up the numbers to compare and we'll get

\ycomparerefs{1}{1}{1}{2}{1.1.-1000\relax 2.-1000\relax}


The fifth argument will be used in case the recursion is started, to be fed again to \xcomparerefs (we add a -1000 at the end to never run out of numbers, as the two sequences can be of different length).

The recursion is started if #1=#3 and both #2 and #4 are different from -1000: in this case we can't decide the lexicographic order, so we chop off the first component and restart.

The final stage will happen when the above condition is not satisfied. If both #2 and #4 are -1000, we output 0 if #1 and #2 are equal (the two original sequences are indeed equal). Otherwise we choose what number to output by comparing #1 and #3.

If #2 and #4 are not both -1000 we again compare #1 and #3: if they are different the two sequences are "far away from each other" and we output 3; otherwise we choose the output by comparing #2 and #4.

The decision macro looks at the two numbers and outputs 1 if the first is one less than the second, 2 if the first is one more than the second and 3 in all other cases.

All is expandable thanks to the \ifboolexpe macro provided by etoolbox.

-
I'm sorry, but your solution is not flexible enough. I've looked at the xparse manual and found the \SplitList argument processor, I tried to get something out of applying \SplitList{.} to the result of \getrefnumber, but the latter seems to always return something of the form {1.1} and \SplitList takes that as a single element (note the braces)... –  mpr Nov 12 '11 at 19:47
@mpr In what sense it's not flexible enough? –  egreg Nov 12 '11 at 20:16
sorry if I've been unclear. I'd like \comparerefs to handle an "arbitrary" amount of nested sectioning environments (ie. an arbitrary amount of terms in "a.b.c.d...") and to be able of comparing them using something similar to "lexicographic ordering". I don't think your solution works under this conditions, and it doesn't check for the length of the references. –  mpr Nov 12 '11 at 20:20
@mpr Please, state in your question what should be done for references having different length. –  egreg Nov 12 '11 at 20:34
I though I had: look at the bottom, where it says "To clarify:", when they're of different lengths, the command should return 3. –  mpr Nov 12 '11 at 20:40

Here is my obligatory pgfkeys answer. It does what you want, minus expandability: it takes reference strings of any length (possibly unequal) and returns 0, 1, 2, or 3 according to whether they are equal, adjacent in the last component, or otherwise. I do assume that their numbers are above -1000: you don't have any very negatively numbered sections, right?

## How it works: high-level

• The code has two parts: first, I convert the strings (e.g. 1.2.3 and 4.5.6) into a form that I like better. I decided early on that I was going to use \foreach to do the looping, and it allows "parallel processing" of the form \x/\y in {1/4, 2/5, 3/6}, so I need to split the references along periods and then recombine them by matching corresponding parts, separated by slashes.

• This operation also gives me the opportunity to fix unequal-length references: I decided that given 1.2.3 and 1.1, the pairs would be 1/1, 2/1, 3/-1000. This is why I assume that sections are reasonably large.

• The second part of the code is the comparison. It works exactly as you'd think: given a foreach loop of the form \x/\y = {1/1, etc.}, I just check successively how \x and \y compare and set the result accordingly. I also check whether I had previously decided that the references were unequal: in that case, the result is always 3, because that means they differed somewhere other than the end. This is always triggered if one of them is shorter than the other.

Now you can try to follow the code. If you get stuck, there's more explanation after:

\documentclass{article}
\usepackage{pgfkeys,pgffor,etoolbox}

\newcommand\comparerefs[2]{%
\pgfkeys{
/split and combine,
reset,
do = {#1.\Stop.}{#2.\Stop.},
}%
\pgfkeys{
/lexicographic,
compare adjacent/.expanded = {\pgfkeysvalueof{/split and combine/list of pairs}},
}%
}

% To protect against \edef
\def\Stop{\noexpand\Stop}

\pgfkeys{
/split and combine/.is family, /split and combine,
do/.code 2 args = {%
\GetPeriod{\beforeone}{\afterone}#1%
\GetPeriod{\beforetwo}{\aftertwo}#2%
\ifdefempty{\beforeone}
{%
\ifdefempty{\beforetwo}
{\pgfkeysalso{next/.style = {}}}
{\pgfkeysalso{list of pairs/.append/.expanded = {,-1000/\beforetwo}}}%
}
{%
\ifdefempty{\beforetwo}
{\pgfkeysalso{list of pairs/.append/.expanded = {,\beforeone/-1000}}}
{\pgfkeysalso{list of pairs/.append/.expanded = {,\beforeone/\beforetwo}}}%
}%
\pgfkeysalso{next}%
},
reset/.style = {
list of pairs/.initial = {0/0},
next/.style = loop,
},
loop/.style = {do/.expanded = {\afterone}{\aftertwo},},
}

% Low-level stuff
\makeatletter
\def\GetPeriod#1#2#3.{%
\ifx\Stop#3%
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
{\def#1{}\def#2{\Stop.}}
{\def#1{#3}\GetToStop#2}%
}
\def\GetToStop#1#2\Stop.{%
\def#1{#2\Stop.}%
}
\makeatother

\pgfkeys{
/handlers/.list pairs/.code = {%
\edef\thiskey{\pgfkeyscurrentpath}%
\foreach \x/\y in {#1} {%
\globaldefs=1%
\pgfkeysalso{\thiskey/.expanded = {\x}{\y}}%
\globaldefs=0%
}%
}
}

\pgfkeys{
/lexicographic/.is family, /lexicographic,
result/.initial = 0,
compare one/.list pairs = {#1},
result
},
compare one/.code 2 args = {%
\ifnumequal{\pgfkeysvalueof{/lexicographic/result}}{0}
{%
\ifnumequal{#1}{#2}
{\pgfkeysalso{result = 0}}
{%
\ifnumequal{#1 + 1}{#2}
{\pgfkeysalso{result = 1}}
{%
\ifnumequal{#1}{#2 + 1}
{\pgfkeysalso{result = 2}}
{\pgfkeysalso{result = 3}}
}%
}%
}%
{%
\pgfkeysalso{result = 3}%
\breakforeach
}%
},
}

\begin{document}
\setlength\parindent{0pt}
\comparerefs{1}{1}

\comparerefs{1}{2}

\comparerefs{2}{1}

\comparerefs{3}{1}

\comparerefs{1.2}{1.2}

\comparerefs{1.2}{1.3}

\comparerefs{1.3}{1.2}

\comparerefs{1.3}{1.1}

\comparerefs{1.2.3}{2.2.3}

\comparerefs{1.2.3}{1.3.3}

\comparerefs{1.2.3}{1.1}

\end{document}


## How it works: low-level

My style with pgfkeys is to put everything in appropriately-named families, so we have two: /split and combine for the first phase, and /lexicographic for the second (it is a kind of lexicographic ordering, I guess).

Here's how /split and combine works:

• It's recursive: there's only one key, do, which fetches the first component of each reference, joins them (inserting -1 as necessary), and then calls itself if either reference still has more parts.

• The fetching is done by a non-pgfkeys macro, \GetPeriod, which has do do some pattern-matching with delimited \def arguments. It works like you'd expect, except that if it happens to find that the reference is out of parts, it doesn't make the after part empty: it puts the termination flag back in and returns it. This way, the recursion can continue blithely until both before parts are empty.

• This also has my favorite trick in the code: \Stop is recursive, but it is also protected from expansion! This way, if \Stop appears in an \edef, it expands to \noexpand\Stop, which expands to \Stop, then stops. This allows me to fully expand the after parts when recursing, and yet not worry about infinite loops or about reinserting the \Stop the next time around. Notwithstanding that I've been pushing pgfkeys as a superior substitute for TeX programming, this is the sort of thing that makes TeX fun.

And here is how /lexicographic works:

• compare adjacent calls compare one repeatedly for every pair of individual reference numbers. That's about it.

• I did have to go and add a handler .list pairs, since pgfkeys doesn't have an interface to the "slash" syntax by default, and this caused me much trouble because \foreach buries its body inside groups so that everything is local. I need result to be set once and for all, though, so that has to be overridden; the solution, as I learned from the answer to Is there a way to set a *global* key value using pgfkeys?, is to set \globaldefs=1. I try not to overdo it; no doubt I am already clobbering the save stack like this.

I am somewhat dissatisfied with the overuse of conditionals. I really, really wanted to use pgfkeys' key-testing logic (.try/.retry) to implement if/then/else statements, but it doesn't work nicely. It is, of course, possible to define a very general "if, then, else" key that does this, but I got lazy and just made it code. I'm still profiting from pgfkeys, though, because of its easy .expanded handler and nice syntax. Just not as much as I'd like.

-
That's some amazing coding/explanation effort! :) I'll have to test (and read) it thoroughly. Regarding -1 appearing in the reference string... I don't know how references to \parts work... but their "level" is -1 (can't really tell how this affects your code though). Regarding 0 appearing in the reference string... I think this indeed could happen, I've seen (and suffered) "Section 0.1" appear in a few manuscripts of mine... what are your thoughts on this matter? –  mpr Nov 13 '11 at 2:57
@mpr: The "level" is -1 but I think their numbers should still be positive. Having a 0 show up in your references means that you wrote a \subsection without enclosing it in a \section, which is wrong, but (like you say) does happen. I don't know how refcount works, though, so let's just change it to -1000 like egreg does. –  Ryan Reich Nov 13 '11 at 3:33
You're quite right, thank you for modifying your code :) It's a tough call, deciding between your answer and @egreg's: I like your explanation better, but I like egre's one including less packages... can't I accept both of them? –  mpr Nov 13 '11 at 6:35