14

Is there a simple way to sort a comma separated list alphabetically in LaTeX? I tried to write a macro (\sortlist{World, Hello} → "Hello, World") using the l3sort documentation example but it completely failed.

\ExplSyntaxOn
\def\@sortlist{}
\newcommand{\sortlist}[1]{
    \clist_set:Nn \l_foo_clist {#1}
    \clist_sort:Nn \l_foo_clist{
      \int_compare:nNnTF { ##1 } > { ##2 }
      { \sort_return_swapped: }
      { \sort_return_same: }
    }
    \def\@sortlist{\l_foo_clist}
}
\ExplSyntaxOff

Is there something like \str_compare to make a string comparison or can l3sort actually only be used for numerical values?

Edit:
How to process a comma separated list? wasn't really helpful for me or rather I wasn't able to solve my problem thereby.

2
  • 3
    Welcome to TeX.SX! Knowing a bit more about your failed attempt and the kind of data you need to sort would be helpful.
    – egreg
    Commented Oct 11, 2016 at 11:30
  • 1
    Sorting strings of ASCII characters is feasible (although non yet supported by expl3); it would become much more complicated if words with accents and so on are involved.
    – egreg
    Commented Oct 11, 2016 at 12:37

5 Answers 5

13

This seems to work, with no packages. EDITED to solve the upper/lower-case problem.

EDIT: Resolved problem when a comparison ran out of letters prior to resolving the order, for example, wash, washer.

See ADDENDUM for handling (after a fashion) diacritics.

\documentclass{article}
\def\listterminator{;}
\makeatletter
\newcommand\alphabubblesort[1]{\def\presorted{}\def\sortedlist{}%
  \sortlist#1,\listterminator,\relax}
\def\sortlist#1#2,#3#4,#5\relax{%
  \if\listterminator#3#4\relax%
    \edef\sortedlist{\sortedlist#1#2}%
  \else
    \ifnum\the\lccode`#1<\the\lccode`#3\relax%
      \edef\sortedlist{\sortedlist\presorted#1#2, }%
      \expandafter\def\expandafter\svfirst\expandafter{\presorted#3#4}%
      \def\presorted{}%
      \expandafter\sortlist\svfirst,#5\relax%
    \else%
      \ifnum`#1=`#3\relax%
        \ifx\relax#2\relax%
          \edef\sortedlist{\sortedlist\presorted#1#2, }%
          \expandafter\def\expandafter\svfirst\expandafter{\presorted#3#4}%
          \def\presorted{}%
          \expandafter\sortlist\svfirst,#5\relax%          
        \else%
          \ifx\relax#4\relax%
            \edef\sortedlist{\sortedlist\presorted#3#4, }%
            \expandafter\def\expandafter\svfirst\expandafter{\presorted#1#2}%
            \def\presorted{}%
            \expandafter\sortlist\svfirst,#5\relax%
          \else        
            \g@addto@macro\presorted{#1}%
            \sortlist#2,#4,#5\relax%
          \fi%
        \fi%
      \else%
        \let\tmp\sortedlist%
        \def\sortedlist{}%
        \expandafter\def\expandafter\svfirst\expandafter{\presorted#3#4}%
        \expandafter\def\expandafter\svsecond\expandafter{\presorted#1#2}%
        \def\presorted{}%
        \expandafter\expandafter\expandafter\expandafter\expandafter%
        \expandafter\expandafter\sortlist\expandafter\expandafter%
        \expandafter\tmp\expandafter\svfirst\expandafter,\svsecond,#5\relax%
      \fi%
    \fi%
  \fi%
}
\makeatother
\begin{document}
\alphabubblesort{book, boot, boat,toad,attic,wish,wash,wasn't,Cat ,Xylophone}
\sortedlist\par

\alphabubblesort{book, washer, boot, boat,toad,attic,wish,wash,wasn't,Cat ,Xylophone}
\sortedlist\par
\end{document}

enter image description here

ADDENDUM

Here's a version that can handle diacritics, in the sense that they do not break the algorithm. This was accomplished by changing the \edefs in the above algorithm to appropriately expanded \defs.

However, diacritics here will always precede all non-diacritic letters in the sort. While maybe not the ideal behavior, it may still be useful.

\documentclass{article}
\def\listterminator{;}
\makeatletter
\newcommand\alphabubblesort[1]{\def\presorted{}\def\sortedlist{}%
  \sortlist#1,\listterminator,\relax}
\def\sortlist#1#2,#3#4,#5\relax{%
  \if\listterminator#3#4\relax%
    \expandafter\def\expandafter\sortedlist\expandafter{\sortedlist#1#2}%
  \else
    \ifnum\the\lccode`#1<\the\lccode`#3\relax%
      \expandafter\expandafter\expandafter\def\expandafter\expandafter%
      \expandafter\sortedlist\expandafter\expandafter\expandafter{%
      \expandafter\sortedlist\presorted#1#2, }%
      \expandafter\def\expandafter\svfirst\expandafter{\presorted#3#4}%
      \def\presorted{}%
      \expandafter\sortlist\svfirst,#5\relax%
    \else%
      \ifnum`#1=`#3\relax%
        \ifx\relax#2\relax%
          \expandafter\expandafter\expandafter\def\expandafter\expandafter%
          \expandafter\sortedlist\expandafter\expandafter\expandafter{%
          \expandafter\sortedlist\presorted#1#2, }%
          \expandafter\def\expandafter\svfirst\expandafter{\presorted#3#4}%
          \def\presorted{}%
          \expandafter\sortlist\svfirst,#5\relax%          
        \else%
          \ifx\relax#4\relax%
            \expandafter\expandafter\expandafter\def\expandafter\expandafter%
            \expandafter\sortedlist\expandafter\expandafter\expandafter{%
            \expandafter\sortedlist\presorted#3#4, }%
            \expandafter\def\expandafter\svfirst\expandafter{\presorted#1#2}%
            \def\presorted{}%
            \expandafter\sortlist\svfirst,#5\relax%
          \else        
            \g@addto@macro\presorted{#1}%
            \sortlist#2,#4,#5\relax%
          \fi%
        \fi%
      \else%
        \let\tmp\sortedlist%
        \def\sortedlist{}%
        \expandafter\def\expandafter\svfirst\expandafter{\presorted#3#4}%
        \expandafter\def\expandafter\svsecond\expandafter{\presorted#1#2}%
        \def\presorted{}%
        \expandafter\expandafter\expandafter\expandafter\expandafter%
        \expandafter\expandafter\sortlist\expandafter\expandafter%
        \expandafter\tmp\expandafter\svfirst\expandafter,\svsecond,#5\relax%
      \fi%
    \fi%
  \fi%
}
\makeatother
\begin{document}
\alphabubblesort{book, washer, w\"asher, boot, boat,toad,attic,wish,wash,wasn't,
  Cat, Xylophone, w\"ash, edifice, \'edifice, w\"asherei}
\sortedlist\par
\end{document}

enter image description here

The algorithm was based on my \bubblesort macro here: Using LaTeX to compact a list of numbers

2
  • 4
    No stackengine: boo hiss. ;-)
    – JPi
    Commented Oct 11, 2016 at 13:40
  • 2
    @JPi I know... it hurts, doesn't it. Commented Oct 11, 2016 at 14:29
16

How about Lua?

\def\sortlist#1{%
  \directlua{%
    local t = { #1 }
    table.sort(t)
    tex.sprint(table.concat(t,", "))
  }%
}

\sortlist{"World", "Hello"}

\bye

enter image description here

Addendum: One of the really useful advantages of this approach is that it is fully expandable, meaning that after

\edef\x{\sortlist{"World", "Hello"}}

the macro \x will contain the sorted list.


When using ConTeXt MKIV you can use the internal settings parser to convert a comma separated list to a Lua table. This has the advantage that you don't need to handle quotes yourself. It also respects grouping with commas inside.

\def\sortlist#1{%
  \ctxlua{%
    local t = utilities.parsers.settings_to_array("\luaescapestring{#1}")
    table.sort(t)
    tex.sprint(table.concat(t,", "))
  }%
}

\starttext
\sortlist{World, Hello, {Entry, with, commas}, "Quotes"}
\stoptext

enter image description here

1
  • We (the TeX community) should definitely start a list of "generic" lua modules / functions, written for context, that could be easily used with other formats (à la luaotfload) with a wrapper. No need to reinvent the wheel.
    – cjorssen
    Commented Oct 12, 2016 at 8:34
11

You can safely compare strings consisting of ASCII characters; words with accented characters will not work.

\documentclass{article}
\usepackage{expl3,xparse}

\ExplSyntaxOn
\prg_new_conditional:Nnn \john_string_if_before:nn { p,T,F,TF }
 {% I hope the LaTeX3 police won't catch me
  \int_compare:nTF { \tex_strcmp:D { #1 } { #2 } < 0 }
   {
    \prg_return_true:
   }
   {
    \prg_return_false:
   }
 }

\NewDocumentCommand{\sortlist}{smm}
 {
  \IfBooleanTF{#1}
   {
    \clist_set:No \l__john_sortlist_data_clist { #2 }
   }
   {
    \clist_set:Nn \l__john_sortlist_data_clist { #2 }
   }
  \john_sortlist:N \l__john_sortlist_data_clist
  \clist_set_eq:NN #3 \l__john_sortlist_data_clist
 }
\clist_new:N \l__john_sortlist_data_clist

\cs_new_protected:Nn \john_sortlist:N
 {
  \clist_sort:Nn #1
   {
    \john_string_if_before:nnTF { ##1 } { ##2 }
     {
      \sort_return_same:
     }
     {
      \sort_return_swapped:
     }
   }
 }
\ExplSyntaxOff

\begin{document}

\sortlist{World,Hello}{\mylistA}

\mylistA

\newcommand{\mylistB}{duck,cat,dog}

\sortlist*{\mylistB}{\mylistC}

\mylistB ${}\to{}$\mylistC

\end{document}

enter image description here

If you want that the comparison is case insensitive, change the definition of \john_sortlist:N into

\cs_new_protected:Nn \john_sortlist:N
 {
  \clist_sort:Nn #1
   {
    \john_string_if_before:nnTF { \str_foldcase:n {##1} } { \str_foldcase:n {##2} }
     {
      \sort_return_same:
     }
     {
      \sort_return_swapped:
     }
   }
 }
4
  • Nice and rather short answer. How can I add an ASCII code inside the existence ASCII codes? e.g. I want to add \"{A} into English ASCII codes after A and before B manually and without any package? (A, \"{A}, B, C, ...)
    – user108724
    Commented Nov 29, 2019 at 8:33
  • @C.F.G Sorry, but that's not covered.
    – egreg
    Commented Nov 29, 2019 at 8:51
  • I hope the LaTeX3 police won't catch me Apparently they caught you \pdftex_strcmp:D seems to be no longer available. Any substitute for this case?
    – Daniel
    Commented Aug 28, 2022 at 20:52
  • Ah, found it: \pdftex_strcmp has been renamed to \tex_strcmp.
    – Daniel
    Commented Aug 28, 2022 at 20:56
3

OpTeX implements merge sort, which is orders of magnitude more powerful than bubblesort (you can feel the difference if you have very large lists, like indexes in books).

Moreover, the sorting is done regards to the actually selected language rules (the sorting of accented characters, compound characters etc.). When English is selected (this is default) and accented letters occur then something like "middle Europe rules" are applied.

\def\sort #1{\bgroup\def\slist{}%
   \nokvx{\ea\addto\ea\slist\ea{\csname+##1\endcsname}\sdef{+##1}{##1}}%
   \readkv{#1}%       % the \slist is prepared
   \_dosorting\slist  % merge sorting is processed here
   \xdef\sortedlist{\ea\puttolist\slist.}%
   \egroup
}
\def\puttolist#1#2{#1\ifx#2.\else, \afterfi{\puttolist#2}\fi}

\sort{aha, uha, muff}
\sortedlist

\sort{book, washer, boot, boat,toad,attic,wish,wash,wasn't,Cat ,Xylophone}
\sortedlist

\sort{book, washer, wäsher, boot, boat,toad,attic,wish,wash,wasn't,
  Cat, Xylophone, wäsh, edifice, édifice, wäsherei}
\sortedlist\par

\bye

You can see in the terminal:

OpTeX: Sorting \slist (en) ...

and the result of the last example is

attic, boat, book, boot, Cat, edifice, édifice, toad, wash, wäsh, washer, wäsher, wäsherei, wasn’t, wish, Xylophone

note the difference from sorted result in the accepted answer.

1

You can try this

This is taken over from https://tex.stackexchange.com/a/273084/4686 (code 5)

% ------------------------------------------------------------------
% Expandable routine to sort strings, based on a QuickSort algorithm 
% and using \pdfstrcmp.
% ------------------------------------------------------------------

% USAGE
% -----

% if using XeTeX:
% \edef\mysortedlist {\QSfull{\mylist}}, or explicit \QSfull {foo, bar, zoo} 


% if using PDFLaTeX: (and inputenc for accented letters)
% \begingroup
%    \subdueutfviiienc
%    \global\edef\mysortedlist {\QSfull{\mylist}}
% \endgroup


\documentclass{article}

\ifdefined\XeTeXinterchartoks
   \let\pdfstrcmp\strcmp
   \usepackage{fontspec}
\else
   \usepackage[T1]{fontenc}
   \usepackage[utf8]{inputenc}% for PDFLaTeX
   \makeatletter
   % this \subdueutfviiienc allows é, à, etc... to survive as is to \edef's.
   \newcommand*\subdueutfviiienc {% to be used in a group
    \count@="C2
    \loop
        \lccode`~\count@
        \lowercase{\def~####1{\noexpand~\string####1}}%
        \ifnum\count@<"E0
        \advance\count@\@ne
    \repeat
    \loop
        \lccode`~\count@
        \lowercase{\def~####1####2{\noexpand~\string####1\string####2}}%
        \ifnum\count@<"F0
        \advance\count@\@ne
    \repeat
    \loop
        \lccode`~\count@
        \lowercase{\def~####1####2####3{\noexpand~\string####1\string####2\string####3}}%
        \ifnum\count@<"F4
        \advance\count@\@ne
    \repeat
   }\makeatother
   % (if inputenc is used with 8bit encoding another approach would be needed)
\fi



% CODE TAKEN FROM https://tex.stackexchange.com/a/273084/4686 (code 5)
% -------------------------------------------------------------------

\makeatletter
\long\def\xintdothis #1#2\xintorthat #3{\fi #1}%
\let\xintorthat \@firstofone
%
% use some (improbable) tokens as delimiters
\catcode`! 3
\catcode`? 3
\catcode`; 3
%
% first we check if empty list (else \qsfull@finish will not find a comma)
% we apply f-expansion to the argument to allow it to be a macro.
%
\def\QSfull #1{\expandafter\qsfull@a\romannumeral-`0#1,!,?}%
%
% first check if input has only blanks, or is empty
\def\qsfull@a #1{\ifx,#1\xintdothis\qsfull@a\fi
                 \ifx!#1\xintdothis\qsfull@abort\fi
                 \xintorthat{\qsfull@start #1}}%
\def\qsfull@abort #1?{}%
%
\def\qsfull@start {\expandafter\qsfull@finish\romannumeral0\qsfull@b,}%
\def\qsfull@finish ,#1{#1}% remove initial ,<space>

\def\qsfull@b ,#1#2,#3{\ifx?#3\xintdothis\qsfull@emptylist\fi
                       \ifx!#3\xintdothis\qsfull@singleton\fi
                       \xintorthat \qsfull@separate@a {}{}#1#2;#3}%
\def\qsfull@emptylist    #1?{}%
\def\qsfull@singleton    #1#2#3;!,?{, #3}%
%
\def\qsfull@separate@a  #1#2#3;#4#5,%
% first pass, remove blanks in passing.
% no need to be extra efficient for that.
{%
    \ifx,#4\expandafter\qsfull@valueisblank\fi
    \ifx!#4\expandafter\qsfull@separate@done\fi
    \if1\pdfstrcmp{#4#5}{#3}%
         \expandafter\qsfull@separate@a@appendtogreater
    \else\expandafter\qsfull@separate@a@appendtosmaller
    \fi
          #4#5?{#1}{#2}#3;%
}%
\def\qsfull@valueisblank \ifx#1\fi,#2?#3#4#5;{\qsfull@separate@a {#3}{#4}#5;#2,}%
\def\qsfull@separate@a@appendtogreater #1?#2{\qsfull@separate@a {#2, #1}}%
%
\def\qsfull@separate@a@appendtosmaller #1?#2#3{\qsfull@separate@a {#2}{#3, #1}}%
%
\def\qsfull@separate@done\if#1\fi #2?#3#4#5;?%
{%
    \qsfull@c #4,!,?, #5\qsfull@c #3,!,?%
}%
% Now that the first pass is done, there are no more blank items.
% In particular here.
\def\qsfull@c ,#1#2,#3{\ifx?#3\xintdothis\qsfull@emptylist\fi
                       \ifx!#3\xintdothis\qsfull@singleton\fi
                       \xintorthat \qsfull@separate {}{}#1#2;#3}%
%
\def\qsfull@separate  #1#2#3;#4#5,% blanks have already been filtered out.
{%
    \ifx!#4\expandafter\qsfull@separate@done\fi
    \if1\pdfstrcmp{#4#5}{#3}%
         \expandafter\qsfull@separate@a@appendtogreater
    \else\expandafter\qsfull@separate@a@appendtosmaller
    \fi
          #4#5?{#1}{#2}#3;%
}%
\def\qsfull@separate@appendtogreater #1?#2{\qsfull@separate {#2, #1}}%
\def\qsfull@separate@appendtosmaller #1?#2#3{\qsfull@separate {#2}{#3, #1}}%
%
%
\catcode`! 12
\catcode`? 12
\catcode`; 12
\makeatother


\begin{document}

\def\mylist{book, washer, wäsher, boot, boat,toad,attic,wish,wash,wasn't,
  Cat, Xylophone, wäsh, edifice, édifice, wäsherei}

% use either a macro \mylist or explicit list as \QSfull argument.
\ifdefined\XeTeXinterchartoks
   \edef\mysortedlist{\QSfull{\mylist}}
\else
   {\subdueutfviiienc\global\edef\mysortedlist{\QSfull{\mylist}}}%
\fi

\mylist\par

\medskip

becomes (notice how whitespaces are normalized in the process):

\medskip

\mysortedlist

\medskip
For some reason due to the comparison being done by \verb|\pdfstrcmp| the é
comes after a-z.

% check
\typeout{\meaning\mylist}

\typeout{\meaning\mysortedlist}

\end{document}

enter image description here

Here in the log output, with PDFLaTeX:

macro:->book, washer, wäsher, boot, boat,toad,attic,wish,wash,wasn't, Cat, Xyl
ophone, wäsh, edifice, édifice, wäsherei
macro:->Cat, Xylophone, attic, boat, book, boot, edifice, toad, wash, washer, w
asn't, wish, wäsh, wäsher, wäsherei, édifice

and using XeLaTeX (which does not cut lines the same way):

macro:->book, washer, wäsher, boot, boat,toad,attic,wish,wash,wasn't, Cat, Xylo
phone, wäsh, edifice, édifice, wäsherei
macro:->Cat, Xylophone, attic, boat, book, boot, edifice, toad, wash, washer, w
asn't, wish, wäsh, wäsher, wäsherei, édifice

The console output (again XeLaTeX) is still different, lines not cut:

macro:->book, washer, wäsher, boot, boat,toad,attic,wish,wash,wasn't, Cat, Xylophone, wäsh, edifice, édifice, wäsherei
macro:->Cat, Xylophone, attic, boat, book, boot, edifice, toad, wash, washer, wasn't, wish, wäsh, wäsher, wäsherei, édifice
1
  • The complications with inputenc-made active characters are due to the fact we need \edef to get complete expansion. But the coding could be modified to achieve complete expansion under repeated expansion of first token. In fact code 3 et 4 of tex.stackexchange.com/a/273084/4686 do that. Then no need for \subdueutfviiienc.
    – user4686
    Commented Oct 12, 2016 at 9:51

You must log in to answer this question.

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