How can I automatically abbreviate page ranges in citations (biblatex)

The page range compression recently feature added to biblatex (version 1.6) has been mentioned a couple times in the existing answers. This answer just provides some details.

Compression is carried out using the command \mkcomprange. It can be applied to both the postnote and pages fields via \DeclareFieldFormat. In postnotes \mkcomprange can handle lists of page ranges provided that they are delimited by commas and/or semicolons.

Behaviour of \mkcomprange can be configured using three different counters. By default:

\setcounter{mincomprange}{10}
\setcounter{maxcomprange}{100000}
\setcounter{mincompwidth}{1}

Compression is applied to any range where the lower limit is greater than mincomprange and has no more digits than maxcomprange. The upper limit in a compressed range is displayed using at least the same number of digits in mincompwidth. An exception to this is when the compressed upper limit has leading zeros; these are suppressed.

\documentclass{article}
\usepackage[T1]{fontenc}
\usepackage[american]{babel}
\usepackage{csquotes}
\usepackage[style=authoryear,maxcitenames=1]{biblatex}

\DeclareFieldFormat{postnote}{\mkcomprange[{\mkpageprefix[pagination]}]{#1}}
\DeclareFieldFormat{pages}{\mkcomprange{#1}}

% Compress ranges where lower limit > 100
\setcounter{mincomprange}{100}

% Don't compress beyond the fourth digit
\setcounter{maxcomprange}{1000}

% Display compressed upper limit with at least two digits,
% unless leading digit is zero
\setcounter{mincompwidth}{10}

\addbibresource{biblatex-examples.bib}

\begin{document}
\cites{salam}[100--110, 101--109, 101--110]{companion} \\
\cite[100--110; 101--110, esp. last paragraph]{companion} \\
\cite[101-110, 1000--1100, 10000--11000]{companion}
\printbibliography
\end{document}

enter image description here


This question was answered in March 2010 by Marco Daniel at the German-speaking forum mrunix.de. Marco used the xstring package and some (in my opinion) ingenious low-level TeX hacks. In the following example, I have used the original code of Marco's core macro \mdprintpages (and marked two minor changes), but removed the need to change the article bibibliography driver (by instead using \DeclareFieldFormat{pages}{\mdprintpages}).

\documentclass{article}

\usepackage{biblatex}

\usepackage{xstring}

% NB: These are much more restrictive than defaults in biblatex.sty
\DeclareRangeChars{-}
\DeclareRangeCommands{\bibrangedash}

\DeclareFieldFormat{pages}{\mkabbrpagerange{#1}}
\DeclareFieldFormat{postnote}{\mkabbrpagerange{#1}}

\newrobustcmd{\mkabbrpagerange}[1]{%
  \ifthenelse{\ifpages{#1}\AND\NOT\ifnumeral{#1}}
    {\ppno\ppspace\mdprintpages{#1}}
    {\mkpageprefix[pagination]{#1}}}

\makeatletter

% START OF ORIGINAL CODE BY MARCO DANIEL
\newif\ifnum@one@empty
\newif\ifnum@two@empty
     \def\mdprintpages#1{% EDITED
             \StrDel{#1}{ }[\md@tempI]% EDITED
             \StrSubstitute{\md@tempI}{\bibrangedash}{,}[\md@tempI]%
             \StrSubstitute{\md@tempI}{--}{,}[\md@tempI]% ADDED
             \StrSubstitute{\md@tempI}{-}{,}[\md@tempI]% ADDED
             \expandafter\expandafter\expandafter\md@printpages\md@tempI,,\@nil}
     \def\md@printpages#1,#2,#3\@nil{%
         \def\num@one{#1}%
         \def\num@two{#2}%
         \ifx\@empty\num@one
            \num@one@emptytrue
           \else
            \num@one@emptyfalse
         \fi
         \ifx\@empty\num@two
              \num@two@emptytrue
          \else
           \num@two@emptyfalse
         \fi
         \ifnum@two@empty
            \ifnum@one@empty
              \else
%                ,\ \num@one% DELETED BY LOCKSTEP
                \num@one% ADDED BY LOCKSTEP
             \fi
          \else
             \StrCompare{\num@one}{\num@two}[\Result]%
%            ,\ \num@one\,--\,\StrGobbleLeft{0\num@two}{\Result}% DELETED BY LOCKSTEP
            \num@one\,--\,\StrGobbleLeft{0\num@two}{\Result}% ADDED BY LOCKSTEP
          \fi
         }
\makeatother
% END OF ORIGINAL CODE BY MARCO DANIEL

\usepackage{filecontents}

\begin{filecontents}{\jobname.bib}
@article{Abel1995,
  author = {S. Abel and M. D. Nguyen and W. Chow and A. Theologis},
  title = {\textit{ACS4}, a primary etc.},
  journaltitle = {J Biol Chem},
  year = {1995},
  volume = {270},
  number = {32},
  pages = {19093--19099},
}
\end{filecontents}

\addbibresource{\jobname.bib}

\begin{document}

Abbreviated page range: \cite[19093-19097]{Abel1995}

Abbreviated page range: \cite[19093--19097]{Abel1995}

Abbreviated page range: \cite[19093 \bibrangedash 19097]{Abel1995}

Page reference, but not a range: \cite[19093]{Abel1995}

Unabbreviated page range: \cite[19093--20093]{Abel1995}

Not recognized as a page range, so the page prefix must be added manually:
\cite[19093, 19094]{Abel1995}
\cite[19092, 19093--19097]{Abel1995}
\cite[19093--19095, last paragraph]{Abel1995}

\printbibliography

\end{document}

Note that there's also a feature request for abbreviated page ranges at SourceForge.net:biblatex.

EDIT 1: Taking a closer look, this is only a partial solution because it only affects the format of page numbers in the bibliography. Hopefully, someone versed in low-level TeX hackery will show how to format citation postnotes the same way.

EDIT 2: The solution now abbreviates pages ranges as postnotes in citations, but ranges are specified only by -. So prefixes must be added manually to page ranges no longer recognized as such. This is restrictive, but avoids having additional material in the postnote being omitted by \mdprintpages.

EDIT 3: The feature request at SourceForge.net:biblatex is now pending and will be implemented in biblatex v1.6. See PLK's comment to his answer for details.

EDIT 4: biblatex 1.6, which includes range compression, was released on July 29th, 2011.


I'm looking into implementing this directly in biber via a biblatex option for the next release. For range "111-118", something like:

rangeend=auto

would get you

111--8

and

rangeend=2

would get

111--18

default would be

rangeend=none

111--118