# zref: Read out first and last reference page per page

I have some paragraph numbers on most of my pages.
I want to write "(1-3)" in the head if the current page contains the paragraph numbers 1,2,3.

Is it possible to read out the first and the last label page per page (with zref? I do not know this package really.)

\documentclass[fontsize=9pt, paper=a5,
]{scrreprt}

\usepackage{marginnote}

% \usepackage[savepos,totpages,titleref,dotfill,counter,user]{zref} % ?

% Paragraph numbers
\newcounter{parnumber}
\newcommand\parnum[1][\theparnumber]{%
\leavevmode%
\refstepcounter{parnumber}%
\marginnote{\theparnumber\label{pn:#1}}% Label
}

% Layout
\usepackage[automark]{scrlayer-scrpage}[]
}

\usepackage{blindtext}
\begin{document}
\chapter{First Page: The Range of parnums should be (1-3) = (xxx-yyy)}
\parnum[TPy]
Theorem of Pythagoras \par
\parnum
\blindtext  \par

Text without parnum.

\parnum Text.

\chapter{Next Page: The Range of parnums should be (4-5)=(xxx-yyy)}
\parnum Text.

\parnum[mylabel] Text.

\chapter{Only one parnum on this page: The Range of parnums should be (6)=(xxx-yyy)}
\parnum Text.
\end{document}

• I'm not sure it is a job for zref. It's easy to do if you are ready to live without the chapter and section titles in headings (my opinion is that it is naturally a job for marks). If you want to keep them, it seems harder to me—unless there is some way to set additional, independent marks that behave like TeX's \firstmark and \botmark. – frougon Jun 18 '19 at 5:47
• @frougon Thanks. OK, I am sorry: I do not say - I had some solutions with marks, but this does not work in special kinds (like the parnum is inside a multicol environment). So I ask myself: Is it possible with zref? – cis Jun 18 '19 at 5:51
• Ah... bad news. I could be mistaken, but I don't see how zref could help. You can get the current value of a property, but I don't see how that would help you to get the first value put on the current page—à la \firstmark, which you need. – frougon Jun 18 '19 at 6:04
• @frougon Mmh, maybe with a good if-methode... But I do not know the package zref well enough. – cis Jun 18 '19 at 6:24
• Unless I'm mistaken, this makes this zref-based method technically superior to any method based on marks, except for the fact that it requires two compilation runs like any \label/\ref-like thing. – frougon Jun 19 '19 at 19:35

The following should do what you want (thanks to Ulrike Fischer for her useful comments!). I used (n--p) style for ranges, but switching to (n-p) style or something similar if you prefer is trivial (just look at \__cis_output_parnum_range:nn: this is the function doing the formatting of parnum info; it should be fairly easy to modify for this kind of change).

\documentclass[fontsize=9pt, paper=a5, twoside, numbers=enddot,
\usepackage{zref}
\usepackage{multicol}
\usepackage{blindtext}
\usepackage{lipsum}
\usepackage{marginnote}
\usepackage{etoolbox}
\usepackage{xparse}

\ExplSyntaxOn

% ****************************************************************************
% *             LaTeX3 bindings to macros from the zref package              *
% ****************************************************************************

% Extract prop #2 recorded for ref cis-parnum-label-#1 (#1 corresponds to a
% parnum)
\cs_new:Npn \cis_extract_prop:nn #1#2
{
% \zref@extract is expandable
\use:c { zref@extract } { cis-parnum-label-#1 } {#2}
}

\cs_generate_variant:Nn \cis_extract_prop:nn { V }

\cs_new_protected:Npn \cis_ref_used:n #1
{
\use:c { zref@refused } { cis-parnum-label-#1 }
}

\cs_generate_variant:Nn \cis_ref_used:n { V }

\prg_new_conditional:Npnn \cis_if_parnum_ref_defined:n #1 { p }
{
% \zref@ifrefundefined is expandable
\use:c { zref@ifrefundefined } { cis-parnum-label-#1 }
{ \prg_return_false: }
{ \prg_return_true: }
}

\prg_generate_conditional_variant:Nnn \cis_if_parnum_ref_defined:n { V } { p }

% ****************************************************************************
% *                Main code for generation of parnum ranges                 *
% ****************************************************************************

% Most important variable: in the \AtBeginDocument hook, we are going to fill
% it to give the parnum range for each page that has at least one parnum. Keys
% in this property list are page numbers (in decimal notation); values have
% the form '{first}{last}' (without quotes). E.g., {2}{5} or {6}{6}. If a page
% contains no parnum at all, the property list will have no key equal to the
% corresponding page number.
\prop_new:N \g_cis_parnums_for_page_prop

\int_new:N \l__cis_fpfep_parnum_int
\int_new:N \l__cis_fpfep_page_in_ref_int
\int_new:N \l__cis_fpfep_last_page_seen_int
\int_new:N \l__cis_fpfep_first_parnum_for_page_int
\int_new:N \l__cis_fpfep_last_parnum_for_page_int

\cs_generate_variant:Nn \int_compare:nNnF { VN }
\cs_generate_variant:Nn \int_compare:nNnF { VNV }

% #1: first labelled parnum in document (if any)
\cs_new_protected:Npn \__cis_find_parnums_for_each_page:n #1
{
\int_set:Nn \l__cis_fpfep_parnum_int {#1} % next parnum to examine
\int_set:Nn \l__cis_fpfep_last_page_seen_int { -1000 } % sentinel value

\bool_while_do:nn
{ \cis_if_parnum_ref_defined_p:V \l__cis_fpfep_parnum_int }
{
% Get the page for parnum \l__cis_fpfep_parnum_int
\int_set:Nn \l__cis_fpfep_page_in_ref_int
{ \cis_extract_prop:Vn \l__cis_fpfep_parnum_int { page } }
\cis_ref_used:V \l__cis_fpfep_parnum_int

\int_compare:VNVF
\l__cis_fpfep_last_page_seen_int = \l__cis_fpfep_page_in_ref_int
{
% The parnum corresponding to the currently examined ref is on a
% different page than the previously-examined parnum, therefore
% we now have all the info for the last seen page -> store it.
\__cis_store_parnums_if_real_page:VVV
\l__cis_fpfep_last_page_seen_int
\l__cis_fpfep_first_parnum_for_page_int
\l__cis_fpfep_last_parnum_for_page_int

\int_set_eq:NN \l__cis_fpfep_first_parnum_for_page_int
\l__cis_fpfep_parnum_int
}

% Will be overwritten if there are more parnums on the same page
\int_set_eq:NN \l__cis_fpfep_last_parnum_for_page_int
\l__cis_fpfep_parnum_int
\int_set_eq:NN \l__cis_fpfep_last_page_seen_int
\l__cis_fpfep_page_in_ref_int
\int_incr:N \l__cis_fpfep_parnum_int
}

% Store info for the last page containing a labelled parnum
\__cis_store_parnums_if_real_page:VVV
\l__cis_fpfep_last_page_seen_int
\l__cis_fpfep_first_parnum_for_page_int
\l__cis_fpfep_last_parnum_for_page_int
}

% #1: page number or sentinel value (-1000) meaning “do nothing”
\cs_new_protected:Npn \__cis_store_parnums_if_real_page:nnn #1#2#3
{
\int_compare:nNnF {#1} = { -1000 }
{
\prop_gput:Nnn \g_cis_parnums_for_page_prop {#1} { {#2} {#3} }
}
}

\cs_generate_variant:Nn \__cis_store_parnums_if_real_page:nnn { VVV }
\cs_generate_variant:Nn \__cis_output_parnum_range:nn { VV }

\tl_new:N \g__cis_oprfp_first_parnum_tl
\tl_new:N \g__cis_oprfp_last_parnum_tl

\cs_new_protected:Npn \cis_output_parnums_range_for_page:n #1
{
\prop_get:NnNTF \g_cis_parnums_for_page_prop {#1} \l_tmpa_tl
{
\seq_set_split:NnV \l_tmpa_seq { } \l_tmpa_tl
\seq_pop_left:NN \l_tmpa_seq \l_tmpa_tl
\seq_pop_left:NN \l_tmpa_seq \l_tmpb_tl

\tl_gset_eq:NN \g__cis_oprfp_first_parnum_tl \l_tmpa_tl
\tl_gset_eq:NN \g__cis_oprfp_last_parnum_tl \l_tmpb_tl
\__cis_output_parnum_range:VV \g__cis_oprfp_first_parnum_tl
\g__cis_oprfp_last_parnum_tl
}
{
% In case there is no parnum at all on a page, the OP wants the page
% header to hold only the last parnum seen (not the last parnum info,
% i.e.: he wants only a number, not a range). Hence "last, last"
% instead of "first, last" here:
\__cis_output_parnum_range:VV \g__cis_oprfp_last_parnum_tl
\g__cis_oprfp_last_parnum_tl
}
}

% Write formatted information for the specified parnum range.
%
% #1: first parnum of range
% #2: last parnum of range
\cs_new_protected:Npn \__cis_output_parnum_range:nn #1#2
{
\tl_if_eq:nnTF {#1} {#2}
{
\tl_if_blank:nF {#1} { (#1) }
}
{
(#1--#2)
}
}

\cs_generate_variant:Nn \cis_output_parnums_range_for_page:n { x }

% Fully expands the argument (which should be a page number)
\NewDocumentCommand \parnumrangeinpage { m }
{
\cis_output_parnums_range_for_page:x {#1}
}

% 1 is the number of the first parnum to consider as possibly existing in the
% document. \__cis_find_parnums_for_each_page:n looks for refs corresponding
% to parnums 1, 2, ... The smallest integer in this scanning process for which
% no ref is found in the .aux file is considered to be one past the last
% parnum.
\AtBeginDocument { \__cis_find_parnums_for_each_page:n { 1 } }

\ExplSyntaxOff

\makeatletter

\zref@newlist{cisparnumlist}

% Paragraph numbers. Must be strictly increasing, and the numbering should
% have no holes.
\newcounter{parnumber}

\newcommand*\parnum[1][]{%
\leavevmode
\refstepcounter{parnumber}%
\ifblank{#1}{}{\label{#1}}% regular LaTeX label for the numbered paragraph
\marginnote{%
\theparnumber
% Set our special zref label for parnums. From the set of all such labels,
% we can deduce in the \AtBeginDocument hook the parnum range for each
% page.
\zref@labelbylist{cis-parnum-label-\the\value{parnumber}}%
{cisparnumlist}}%
}

\makeatother

% Allow easy on/off switching of parnum info in headers

% Set up the page headers
\usepackage[automark]{scrlayer-scrpage}
\marginnote{\parnumrangeinpage{\thepage}}% range of parnums
}{}%
}

\begin{document}

\chapter{Three parnums here}

\parnum[TPy]
Theorem of Pythagoras \par
\parnum
\blindtext  \par

Text without parnum.

\parnum Text.

The theorem of Pythagoras corresponds to parnum~\ref{TPy} on
page~\pageref{TPy}.

\chapter{Two parnums here}

\parnum Text.

\parnum[mylabel] Text.

\chapter{Only one parnum here}

\parnum Text.

\newpage % Test repeating of last parnum info when there is no parnum on the
% current page.
\chapter{Test with parnums deeply embedded in \TeX\ lists}

\noindent
{\centering
\begin{minipage}{\linewidth}
\begin{multicols}{2}
\parnum Text.

\lipsum[1-3]

\parnum Text.
\end{multicols}
\end{minipage}
}
\newpage
\lipsum[1-2]

\end{document}


• Well, from your commment, I had understood that on pages with no parnum at all, you wanted the header to show the last parnum info, instead of just the last parnum. This is fixed, see the latest version of my answer. – frougon Jun 19 '19 at 5:39
• The main work is to find the (first parnum, last parnum) pair for each page. Once that's done, the rest is essentially formatting. It would have been easier with marks such as \firstmark and \botmark—they were designed for that—but that wasn't the question and LaTeX already uses them for running heads, which you do use. There might be some other way with titleps though, if this works with the KOMA classes (but it's out of the question scope). – frougon Jun 19 '19 at 7:09
• (no time to look at this link now, sorry) Concerning my answer, I have fixed broken indentation in the main function (\__cis_find_parnums_for_each_page:n), simplified it a little bit (there was a redundant \int_set_eq:NN) and expanded a comment to ease understanding; so, you should probably fetch the code again. The behavior should be strictly unchanged, though. – frougon Jun 19 '19 at 8:13
• Yes, this is the purpose of the \tl_if_blank:VF test in \__cis_output_parnum_range:NN :-) (\tl_new:N initializes \g__cis_oprfp_first_parnum_tl and \g__cis_oprfp_last_parnum_tl as empty). – frougon Jun 19 '19 at 8:31
• I added a toggle (parnumsinheader) and simple commands (\parnumsinheaderon and \parnumsinheaderoff) to allow easy on/off switching of parnum info in headers. This should cover the basic needs. Also removed a stray [] after your \usepackage[automark]{scrlayer-scrpage}, and improved a few comments. – frougon Jun 19 '19 at 9:30