Secondary sort for array

Table of contents

Sorting

Sorting has always been one of the most commonly performed tasks for many of us, especially those dealing with large sets of data. In JavaScript, this is no exception, even for web developers. Luckily enough, they do not have to implement their own sorting algorithm if the native sort method that provides out of the box is already good enough to get the job done.

Dealing with sorting with large sets of data, such as a list of user records or shopping items, users usually want to do different kinds of sorting to see which data tops the list. It is pretty easy to do sorting on a list of data with each containing its unique set of values in each field. In reality, there is a high chance that more than one item could have a field which requires sorting and contains the exact same value. As such, it is difficult to do proper sorting on those kinds of data as we do not have enough information to perform sorting when the field values are the same.

What if we could actually do a secondary sorting based on another field such as the name or ID?

A piece of code is worth a million words

const productList = [
  {
    "price": 242,
    "productId": 973,
    "createdAt": "2017-02-20T13:08:55.799Z",
    "stock": 501
  },
  {
    "price": 331,
    "productId": 514,
    "createdAt": "1985-10-30T10:03:55.658Z",
    "stock": 875
  },
  {
    "price": 958,
    "productId": 953,
    "createdAt": "1992-12-15T10:07:58.691Z",
    "stock": 335
  },
  {
    "price": 547,
    "productId": 585,
    "createdAt": "1996-01-20T16:54:16.601Z",
    "stock": 591
  },
  {
    "price": 820,
    "productId": 420,
    "createdAt": "2005-02-01T15:31:01.768Z",
    "stock": 494
  },
  {
    "price": 945,
    "productId": 524,
    "createdAt": "2018-06-05T13:46:51.857Z",
    "stock": 76
  },
  {
    "price": 242,
    "productId": 144,
    "createdAt": "1993-11-20T13:14:22.462Z",
    "stock": 697
  },
  {
    "price": 331,
    "productId": 408,
    "createdAt": "1982-01-20T02:27:53.115Z",
    "stock": 18
  }
];

productList is a list of unsorted products that contains four fields - price, productId, createdAt, and stock. Some of the products have the exact same price, such as 242 and 331. Let's see what the output will look like if we perform sorting based on the field price.

function sortingFunction(a, b) {
  return a.price - b.price; /** Sort in ascending order */
}

/** Sorting mutates the original array, doing a clone here to ensure original one stays untouched */
const clonedProductList = productList.slice();
clonedProductList.sort(sortingFunction);
console.log(clonedProductList);
// Sorted list:
// [
//   {
//     "price": 242,
//     "productId": 973,
//     "createdAt": "2017-02-20T13:08:55.799Z",
//     "stock": 501
//   },
//   {
//     "price": 242,
//     "productId": 144,
//     "createdAt": "1993-11-20T13:14:22.462Z",
//     "stock": 697
//   },
//   {
//     "price": 331,
//     "productId": 514,
//     "createdAt": "1985-10-30T10:03:55.658Z",
//     "stock": 875
//   },
//   {
//     "price": 331,
//     "productId": 408,
//     "createdAt": "1982-01-20T02:27:53.115Z",
//     "stock": 18
//   },
//   {
//     "price": 547,
//     "productId": 585,
//     "createdAt": "1996-01-20T16:54:16.601Z",
//     "stock": 591
//   },
//   {
//     "price": 820,
//     "productId": 420,
//     "createdAt": "2005-02-01T15:31:01.768Z",
//     "stock": 494
//   },
//   {
//     "price": 945,
//     "productId": 524,
//     "createdAt": "2018-06-05T13:46:51.857Z",
//     "stock": 76
//   },
//   {
//     "price": 958,
//     "productId": 953,
//     "createdAt": "1992-12-15T10:07:58.691Z",
//     "stock": 335
//   }
// ]

We can see that it is a stable sort, as the list items with the same field value appear in the same order in the sorted output. That looks good to me, but this is not the purpose of today's topic. What we want to see is a secondary sort on list items that have the same field values.

function sortWithSecondarySort(a, b) {
  const primarySort = a.price - b.price; /** Sort by `price`, in ascending order */
  
  return primarySort || (a.productId - b.productId); /** Secondary sort by `productId`, in ascending order */
}

/** Sorting mutates the original array, doing a clone here to ensure original one stays untouched */
const clonedProductList = productList.slice();
clonedProductList.sort(sortingFunction);
console.log(clonedProductList);
// Sorted output with secondary sort:
// [
//   {
//     "price": 242,
//     "productId": 144,
//     "createdAt": "1993-11-20T13:14:22.462Z",
//     "stock": 697
//   },
//   {
//     "price": 242,
//     "productId": 973,
//     "createdAt": "2017-02-20T13:08:55.799Z",
//     "stock": 501
//   },
//   {
//     "price": 331,
//     "productId": 408,
//     "createdAt": "1982-01-20T02:27:53.115Z",
//     "stock": 18
//   },
//   {
//     "price": 331,
//     "productId": 514,
//     "createdAt": "1985-10-30T10:03:55.658Z",
//     "stock": 875
//   },
//   {
//     "price": 547,
//     "productId": 585,
//     "createdAt": "1996-01-20T16:54:16.601Z",
//     "stock": 591
//   },
//   {
//     "price": 820,
//     "productId": 420,
//     "createdAt": "2005-02-01T15:31:01.768Z",
//     "stock": 494
//   },
//   {
//     "price": 945,
//     "productId": 524,
//     "createdAt": "2018-06-05T13:46:51.857Z",
//     "stock": 76
//   },
//   {
//     "price": 958,
//     "productId": 953,
//     "createdAt": "1992-12-15T10:07:58.691Z",
//     "stock": 335
//   }
// ]

Great! You can now see the difference is that the first two items are no longer in the same order after performing a secondary sort on their productId field. A basic compare function basically needs to know which value is greater than or lesser than the other. Sometimes, it could return 0, which indicates no differences between the two items during comparison. 0 is one of the falsy values in JavaScript, so we can make use of the fact to add a fallback using the || operator to add a secondary sort. With such a technique, you can technically add even more comparisons if the secondary sort returns a 0 as well.

Wrap-up

Sorting an array or any other large sets of sortable data is a common task for software engineers, regardless of the kind of task they are dealing with.

It is important to note that sometimes a primary sort key is not enough to fulfill certain use cases. You might need to ensure that you always have a secondary sort (and more, probably) that might come in handy under some circumstances. For instance, a stable sort algorithm might not always be used, even in a native sorting method that a language or a system provides.

In JavaScript, we can make use of the fact that a falsy value, 0, will be returned for two items that are exactly the same during comparison. The || operator allows fallback to the next expression, as shown in the example above.

That's it! Have a wonderful day ahead, and I'll see you in the upcoming blog post. Peace! ✌️