16

This a bit out of ordinary. But has anybody used LaTeX to compact a list of numbers?

The closest example that comes to mind is the way the citation numbers are compressed and sorted using the cite package. So, if I write \cite{key1, key2, key3, key4, key5}, we get something like 1-3, 7, 9.

The situation under consideration is almost similar, except the numbers are real numbers or expanded to real numbers, not citation keys. So, if I write \compactthis{1,2,3,4,5,9,8,7, 11}, I will get 1-5, 7-9, 11.

Is there a quick way of achieving this? Writing macros from scratch will take time which I can not afford at this moment.

(I understand that programming languages are better suited for this, but my current situation demands the use of LaTeX.)

0

3 Answers 3

12

EDITED to sort the input stream using a bubble sort (the macro \bubblesort can be used independently of the \compactthis macro, if desired).

A \listterminator must be set to ANY numerical value known not to be in the list (e.g., suitably large, negative, zero, etc.), currently set at 9999. As shown (both in code and demonstrated in the MWE), the macro \adjtie can be set to -- if 1--2 is preferred to 1, 2 for adjacent limiting entries.

No packages required.

\documentclass{article}
%
% THIS CODE CAN \bubblesort A NUMBERED LIST AND THEN \compactthis LIST IN THE MANNER
% OF 1-3, 7, 11-13
%
\def\listterminator{9999}% SET TO *ANY* VALUE KNOWN NOT TO BE IN LIST (POSITIVE OR NEGATIVE)
\def\adjtie{, }
%\def\adjtie{--}% OPTIONAL IF 1--2 preferred over 1, 2
\newcommand\compactthis[1]{%
  \bubblesort{#1}%
  \expandafter\begincompaction\sortedlist,\listterminator,\relax%
}
\def\begincompaction#1,#2\relax{%
  \def\startlist{#1}%
  \def\currentendlist{#1}%
  \findendlist#2\relax%
}
\def\findendlist#1,#2\relax{%
  \ifnum\numexpr\currentendlist+1\relax=#1\relax%
    \def\currentendlist{#1}%
    \findendlist#2\relax%
  \else%
    \ifnum\startlist=\currentendlist\relax%
      \ignorespaces\startlist\unskip%
    \else%
      \ifnum\numexpr\startlist+1\relax=\currentendlist\relax%
        \ignorespaces\startlist\unskip\adjtie\ignorespaces\currentendlist\unskip%
      \else%
        \ignorespaces\startlist\unskip--\ignorespaces\currentendlist\unskip%
      \fi%
    \fi%
    \ifnum#1=\listterminator\else,\ \begincompaction#1,#2\relax\fi%
  \fi%
}
\newcommand\bubblesort[1]{\def\sortedlist{}\sortlist#1,\listterminator,\relax}
\def\sortlist#1,#2,#3\relax{%
  \ifnum#2=\listterminator\relax%
    \edef\sortedlist{\sortedlist#1}%
  \else
    \ifnum#1<#2\relax%
      \edef\sortedlist{\sortedlist#1,}%
      \sortlist#2,#3\relax%
    \else%
      \let\tmp\sortedlist%
      \def\sortedlist{}%
      \expandafter\sortlist\tmp#2,#1,#3\relax%
    \fi%
  \fi%
}
\begin{document}
Bubble Sort Demonstration: 
\bubblesort{1,2,11, 7, 4, 3}\sortedlist\par
\compactthis{1,2,3,4,5,7,8,9, 11}\par
\compactthis{1,2, 12 ,13 ,18, 20} (single member)\par
\compactthis{1,2, 12,13,18, 19, 20} (range at end)\par
\def\adjtie{--}\compactthis{1,2, 12,13,18, 19, 20} (\verb|\adjtie| set to {--})\par
\compactthis{1,2,11, 7, 4, 3, 12, 14, 13} (unsorted input)
\end{document}

enter image description here

2
  • A comprehensive solution ready-to-use.
    – Masroor
    Aug 23, 2016 at 17:35
  • @Masroor Thank you. I appreciate the consideration and endorsement. Aug 23, 2016 at 17:37
15

Here is the bulk of what you need with the input in black and the output in red:

enter image description here

Notes:

Code:

\documentclass{article}
\usepackage{pgffor}
\usepackage{xstring}
\usepackage{etoolbox}
\usepackage{xcolor}

\newtoggle{StartedRange}
\newcommand{\LastNumber}{}%
\newcommand{\LastRangeStart}{}%
\newcommand*{\compactthis}[1]{%
    \edef\ExpandedParam{#1}% <-- Apply sorting here
    \ExpandedParam:
    \begingroup\color{red}%
    \togglefalse{StartedRange}%
    \foreach \x in \ExpandedParam {%
        \iftoggle{StartedRange}{%
            \pgfmathtruncatemacro\ExpectedNextNumber{\LastNumber+1}%
            \IfEq{\ExpectedNextNumber}{\x}{%
                %% Continue this range
            }{%
                \IfEq{\LastRangeStart}{\LastNumber}{%
                    %% Was a single member
                }{%
                    --\LastNumber% Close last range
                }%
                ,\, \x%        and start a new range
                \xdef\LastRangeStart{\x}%
            }%
       }{%
            \x% initial range
            \xdef\LastRangeStart{\x}%
            \global\toggletrue{StartedRange}%
        }%
        \xdef\LastNumber{\x}%
    }%
    %  Process any ranges at end of list:
    \IfEq{\LastRangeStart}{\LastNumber}{%
        %% Was a single member
    }{%
        --\LastNumber% Close last range
    }%
    \endgroup%
}%

\begin{document}
\compactthis{1,2,3,4,5,7,8,9, 11}

\compactthis{1,2, 12,13,18, 20}% Single member

\compactthis{1,2, 12,13,18, 19, 20}% range at end

\end{document}
4
  • 1
    Highly procedural approach :) I'm looking forward to someone posting a functional solution (I'm really not able to do that myself)
    – yo'
    Aug 23, 2016 at 9:39
  • @yo': Yep, but gets the job done. :-) Aug 23, 2016 at 10:45
  • It wasn't criticism! It was just ... well ... a comment; I'd really like to see a functional answer...
    – yo'
    Aug 23, 2016 at 11:45
  • Also, I'd not "compactify" 1, 2 to 1--2, but that's a matter of taste.
    – yo'
    Aug 23, 2016 at 11:47
7

Here is a LuaLaTeX solution. Because Lua has an easy-to-use sorting function (table.sort), it also sorts the input list. There are probably ways to make the Lua code more concise, but it should be easy to understand for anyone familiar with imperative programming languages.

\documentclass{article}
\usepackage{luacode}

\begin{luacode*}
function print_range(range_min, range_max)
  if range_min == range_max then
    tex.sprint(tostring(range_min))
  else
    tex.sprint(tostring(range_min) .. "--" .. tostring(range_max))
  end
end
function compactthis(...)
  local numbers = {...}
  table.sort(numbers)
  local range_started = false
  local range_min = 0
  local range_max = 0
  for i = 1, #numbers do
    if range_started then
      if numbers[i] <= range_max + 1 then
        range_max = numbers[i]
      else
        print_range(range_min, range_max)
        range_started = false
      end
    end
    if not range_started then
      if i ~= 1 then
        tex.sprint(", ")
      end
      range_started = true
      range_min = numbers[i]
      range_max = numbers[i]
    end
  end
  if range_started then
    print_range(range_min, range_max)
  end
end
\end{luacode*}
\newcommand\compactthis[1]{\luaexec{compactthis(#1)}}

\begin{document}
\compactthis{1,2,3,4,5,9,8,7,11}
\end{document}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .