3

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"}
}
Syed
  • 52,495
  • 4
  • 30
  • 85
Andrew
  • 10,569
  • 5
  • 51
  • 104
  • 3
    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 – user1066 Nov 21 '22 at 22:57

3 Answers3

6
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 *)

Bob Hanlon
  • 157,611
  • 7
  • 77
  • 198
5

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

user293787
  • 11,833
  • 10
  • 28
3

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

Syed
  • 52,495
  • 4
  • 30
  • 85