Warm tip: This article is reproduced from stackoverflow.com, please click
javascript sorting typescript

Sorting an array of objects but prioritising certain items

发布于 2020-03-31 22:57:14

Here's a sample data:

[
  {
    "name": "test",
    "is_folder": "Y",
    "is_file": "N",
    "sort": 0,
    "sort_reverse": 1
  },
  {
    "name": "1.jpg",
    "is_folder": "N",
    "is_file": "Y",
    "sort": 1,
    "sort_reverse": 0
  }
]

The idea is that the folders will always be at the top of the list followed by the files but I also want the folders and files to be sorted.

Example result:

Ascending (Default):

  • Folder 1
  • Folder 2
  • Folder 3
  • 1.jpg
  • 2.jpg
  • 3.jpg

Descending (Reverse):

  • Folder 3
  • Folder 2
  • Folder 1
  • 3.jpg
  • 2.jpg
  • 1.jpg

How can I achieve this with my sample data?

Here is what I've got so far to sort the names but no taking into account the folder/file order:

items.sort((a: any, b: any) => {
    if (a.name > b.name) {
        return 1;
    } else if (a.name < b.name) {
        return -1;
    } else {
        return 0;
    }
});
Questioner
J.Do
Viewed
38
VLAZ 2020-01-31 23:54

You can simply do the following:

  • If the is_folder property is equal, then they are both files or both are folders. Compare their names
  • Otherwise sort is_folder === "Y" higher

Ascending:

const arr = [
  { "name": "Folder 3",  "is_folder": "Y" },
  { "name": "2.jpg",     "is_folder": "N" },
  { "name": "Folder 2",  "is_folder": "Y" },
  { "name": "Folder 10", "is_folder": "Y" },
  { "name": "Folder 1",  "is_folder": "Y" },
  { "name": "15.jpg",    "is_folder": "N" },
  { "name": "3.jpg",     "is_folder": "N" },
  { "name": "abc.txt",   "is_folder": "N" },
  { "name": "1.jpg",     "is_folder": "N" },
];

arr.sort((a, b) => {
  if (a.is_folder === b.is_folder)
    return a.name.localeCompare(b.name, undefined, {numeric: true});
    
  if (a.is_folder === "Y")
    return -1;
    
  if (b.is_folder === "Y")
    return 1;
    
  return 0;
});

console.log(arr)

Descending:

const arr = [
  { "name": "Folder 3",  "is_folder": "Y" },
  { "name": "2.jpg",     "is_folder": "N" },
  { "name": "Folder 2",  "is_folder": "Y" },
  { "name": "Folder 10", "is_folder": "Y" },
  { "name": "Folder 1",  "is_folder": "Y" },
  { "name": "15.jpg",    "is_folder": "N" },
  { "name": "3.jpg",     "is_folder": "N" },
  { "name": "abc.txt",   "is_folder": "N" },
  { "name": "1.jpg",     "is_folder": "N" },
];

arr.sort((a, b) => {
  if (a.is_folder === b.is_folder)
    return b.name.localeCompare(a.name, undefined, {numeric: true}); //<-- flip `a` and `b`
    
  if (a.is_folder === "Y")
    return -1;
    
  if (b.is_folder === "Y")
    return 1;
    
  return 0;
});

console.log(arr)

See on TypeScript Playground (including types)

Using localeCompare with the numeric collation option ensures that numbers will be correctly sorted where 10 is after 2, for example. Here is what happens if you don't use that:

const arr = [
  { "name": "Folder 3",  "is_folder": "Y" },
  { "name": "2.jpg",     "is_folder": "N" },
  { "name": "Folder 2",  "is_folder": "Y" },
  { "name": "Folder 10", "is_folder": "Y" },
  { "name": "Folder 1",  "is_folder": "Y" },
  { "name": "15.jpg",    "is_folder": "N" },
  { "name": "3.jpg",     "is_folder": "N" },
  { "name": "abc.txt",   "is_folder": "N" },
  { "name": "1.jpg",     "is_folder": "N" },
];

arr.sort((a, b) => {
  if (a.is_folder === b.is_folder)
    return a.name.localeCompare(b.name); //<-- no numeric collation
    
  if (a.is_folder === "Y")
    return -1;
    
  if (b.is_folder === "Y")
    return 1;
});

console.log(arr)

As you can see, we basically repeat all the code to do the ascending/descending sort. This makes maintaining it harder, however we can improve it. We can extract each part into a separate function:

  • sort names of similar items - ascending
  • sort names of similar items - descending
  • sort folders before files

Luckily, for ascending/descending flipping the order of a and b is the same as multiplying by -1 since localeCompare returns a number - positive, negative, or zero. So, we can only have the logic once and not repeat it twice:

const compareFoldersFirst = (a, b) => {
  if (a.is_folder === "Y")
    return -1;

  if (b.is_folder === "Y")
    return 1;

  return 0;
}

const compareNameAsc = (a, b) => {
  if (a.is_folder === b.is_folder)
    return a.name.localeCompare(b.name, undefined, {numeric: true});

  return 0;
}

const compareNameDesc = (a, b) => compareNameAsc(a, b) * -1;

We can actually generalise the logic used in compareNameDesc - it just runs a function with two parameters and multiplies it by -1, so we can make a generic reverse function that can reverse any sorting order:

const reverse = compareFn => (a, b) => compareFn(a, b) * -1;
const compareNameDesc = reverse(compareNameAsc);

In addition, we can slightly change the logic of each comparison to make it completely self sufficient, since right now the name sorting depends on whether something is a folder or not.

const compareFoldersFirst = (a, b) => {
  if (a.is_folder === b.is_folder)
    return 0;

  if (a.is_folder === "Y")
    return -1;

  if (b.is_folder === "Y")
    return 1;
};

This is even shorter to express with a slight..."creative usage" of the boolean and number conversion rules:

const compareFoldersFirst = (a, b) => Number(b.is_folder === "Y") - Number(a.is_folder === "Y");

At any rate, this allows us to drop the is_folder check from the name comparison and we're left with simply the following comparers:

const compareFoldersFirst = (a, b) => Number(b.is_folder === "Y") - Number(a.is_folder === "Y");
const compareNameAsc = (a, b) => a.name.localeCompare(b.name, undefined, {numeric: true});
const compareNameDesc = reverse(compareNameAsc);

We have almost all the tools we need to produce any sorting order we want by adding more logic once and then reverse it or not. We just need to be able to easily compose the different comparers. To do that, we can generalise the sorting as the following: We get any number of sorting functions. We produce a new function that will run them one by one until one returns a non-zero result at which point we return that. This can be done as

const comparer = (...comparers) => 
  (a, b) => {
    for(let compareFn of comparers){
       const result = compareFn(a, b);
       if (result !== 0) 
         return result;
    }
  }

But can be made more compact using Array#reduce. In the end the code is easier to maintain and composable, using the helper compare function:

const arr = [
  { "name": "Folder 3",  "is_folder": "Y" },
  { "name": "2.jpg",     "is_folder": "N" },
  { "name": "Folder 2",  "is_folder": "Y" },
  { "name": "Folder 10", "is_folder": "Y" },
  { "name": "Folder 1",  "is_folder": "Y" },
  { "name": "15.jpg",    "is_folder": "N" },
  { "name": "3.jpg",     "is_folder": "N" },
  { "name": "abc.txt",   "is_folder": "N" },
  { "name": "1.jpg",     "is_folder": "N" },
];

//helper function that takes any amount of compare functions 
//produces a function that runs each until a non-zero result 
const compare = (...comparers) => 
  (a, b) => comparers.reduce(
    (result, compareFn) => result || compareFn(a, b),
    0
  );

//reverse the result of any compare function after it runs:
const reverse = compareFn  => (a, b) => compareFn (a, b) * -1;

//the basic comparer functions:
const compareFoldersFirst = (a, b) => Number(b.is_folder === "Y") - Number(a.is_folder === "Y");
const compareNameAsc = (a, b) => a.name.localeCompare(b.name, undefined, {numeric: true});
const compareNameDesc = reverse(compareNameAsc);

//final comparison function derived form the basic ones
const asc = compare(
  compareFoldersFirst,
  compareNameAsc
);

const desc = compare(
  compareFoldersFirst,
  compareNameDesc
);

console.log("--- Ascending sort ---\n",  arr.sort(asc));
console.log("--- Descending sort ---\n", arr.sort(desc));

See on TypeScript Playground (including types)