# Sorting a list based only on a certain property of the first element of sublists

Suppose I have a list called roster. Each element in roster is a list containing two strings: (i) the species of animal and (ii) the animal's name.

roster = {
{"cat", "Garfield"},
{"cat", "Cheshire"},
{"dog", "Pongo"},
{"dog", "Lassie"},
{"elephant", "Horton"},
{"elephant", "Babar"}
};


I want to sort roster in descending order by species size: elephants are largest, dogs are smaller, and cats are smallest. However, in doing the sort, I want to keep the order of names unchanged.

One way to do this is to apply an ordering list to roster:

roster[[{5, 6, 3, 4, 1, 2}]]


{{"elephant", "Horton"}, {"elephant", "Babar"}, {"dog", "Pongo"}, {"dog", "Lassie"}, {"cat", "Garfield"}, {"cat", "Cheshire"}}

which is the desired output.

However, I'd like to achieve this result more programmatically.

I've considered using SortBy[list, f], which according to the documentation, "sorts the elements of list in the order defined by applying f to each of them." I've tried the following:

(* Sort in decending order by size: elephant (largest), dog, cat (smallest) *)
sortFunc[animals_List] := Which[#[[1]] == "elephant", 1, #[[2]] == "dog", 2, #[[3]] == "cat", 3] &;
SortBy[roster, sortFunc]


which gives the correct species order, but also sorts the elements so the animal names are in canonical order, which is not what I want. I want the names to appear in whatever order they are given in roster. How can I do this?

I would like the method to be usable in Mathematica 9.0+, so please do not use Associations or other "new" built-in features (with "new" being features implemented in Mathematica within the past 8-10 years).

Additionally, I would like the method to be generalizable to nonconsecutive species in roster. For example, the following

roster = {
{"cat", "Garfield"},
{"cat", "Cheshire"},
{"dog", "Pongo"},
{"dog", "Lassie"},
{"elephant", "Horton"},
{"elephant", "Babar"},
{"dog", "Perdita"},
{"dog", "Snowy"},
{"dog", "Odie"}
};


should be sorted as the following (i.e., leaving the order of names unchanged within each species):

{
{"elephant", "Horton"},
{"elephant", "Babar"},
{"dog", "Pongo"},
{"dog", "Lassie"},
{"dog", "Perdita"},
{"dog", "Snowy"},
{"dog", "Odie"},
{"cat", "Garfield"},
{"cat", "Cheshire"}
}

• In addition, using the sortFunc given by @Bob Hanlon, compare SortBy[roster, {sortFunc}] (giving a stable sort) with SortBy[roster, sortFunc]. As I understand things from this answer, SortBy[list, {fn}] gives a stable sort (ties are not sorted) whereas SortBy[list, fn] will sort ties Nov 21, 2022 at 22:57

Clear["Global*"]


Change sortFunc to

sortFunc :=
Which[#[[1]] == "elephant", 1, #[[1]] == "dog", 2, #[[1]] == "cat",
3, True, 4] &;


Use the alternate syntax for SortBy

SortBy[{{"elephant", "Horton"}, {"elephant", "Babar"}, {"dog",
"Pongo"}, {"dog", "Lassie"}, {"cat", "Garfield"}, {"cat",
"Cheshire"}}, {sortFunc, First}]

(* {{"elephant", "Horton"}, {"elephant", "Babar"}, {"dog", "Pongo"}, {"dog",
"Lassie"}, {"cat", "Garfield"}, {"cat", "Cheshire"}} *)


Checking against stated desired output

% === {{"elephant", "Horton"}, {"elephant", "Babar"}, {"dog",
"Pongo"}, {"dog", "Lassie"}, {"cat", "Garfield"}, {"cat", "Cheshire"}}

(* True *)


One can use

mySortBy[list_,f_] := list[[Ordering[Map[f,list]]]];


Example.

roster = {
{"cat", "Garfield"},
{"cat", "Cheshire"},
{"dog", "Pongo"},
{"dog", "Lassie"},
{"elephant", "Horton"},
{"elephant", "Babar"},
{"dog", "Perdita"},
{"dog", "Snowy"},
{"dog", "Odie"}
};

sortFunc[{animal_,_}] := Switch[animal,
"elephant",1,
"dog",2,
"cat",3];

mySortBy[roster,sortFunc]
(* {{elephant,Horton},
{elephant,Babar},
{dog,Pongo},
{dog,Lassie},
{dog,Perdita},
{dog,Snowy},
{dog,Odie},
{cat,Garfield},
{cat,Cheshire}} *)


To make it more general, Interpreter functionality can be utilized:

Clear["Global*"]
animals = {"elephant", "dog", "cat", "mouse", "goat", "horse",
"Horse"};
f[a_String] :=
Mean@First@First@Interpreter["Animal"][ToLowerCase@a]["Weight"]
Transpose[{#, f@#}] & /@ animals


{{"elephant", 4.110^6}, {"dog", 3.710^4}, {"cat", 4750.}, {"mouse", 26.}, {"goat", 6.410^4}, {"horse", 5.810^5}, {"Horse", 5.8*10^5}}

Notice capitalization on "Horse". ToLowerCase had to be added to the function to allow proper sorting. Such a case may appear in the input.

I have taken the liberty to add a few entries to the data in the OP:

roster = {{"cat", "Garfield"}, {"mouse", "Dodgy"}, {"cat",
"Cheshire"}, {"dog", "Pongo"}, {"dog", "Lassie"}, {"goat",
"Tod"}, {"horse", "Cruiser"}, {"mouse", "Cheesy"}, {"elephant",
"Horton"}, {"Horse", "Forge"}, {"elephant", "Babar"}, {"dog",
"Perdita"}, {"dog", "Snowy"}, {"dog", "Odie"}, {"goat",
"Caper"}, {"dog", "Pearl"}
};

(Column /@ {roster, ReverseSortBy[{f@First@# &}][roster]})