3
$\begingroup$

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"}
}
$\endgroup$
1
  • 3
    $\begingroup$ 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 $\endgroup$
    – user1066
    Nov 21, 2022 at 22:57

3 Answers 3

6
$\begingroup$
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 *)
$\endgroup$
5
$\begingroup$

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}} *)
$\endgroup$
3
$\begingroup$

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]})

enter image description here

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.