Sorting data is a fundamental task in software development, often seamlessly handled by databases or language features. But what about when we step outside the comfort of SQL queries and face the need to manually implement similar functionality in JavaScript, especially for multilevel sorting? This challenge is not just academic—it’s a practical scenario that developers frequently encounter.

Imagine you’re tasked with displaying student records on a web page, sorted by their city and then by age in descending order. How would you replicate the sophisticated sorting mechanism of a SQL query like the following using JavaScript?

SELECT * FROM STUDENTS ORDER BY CLASS, GRADE DESC, AGE DESC;

This article shares insights from developing an in-memory version of a database repository for quicker testing, which involved sorting entries by multiple object attributes. Whether you’re refining a front-end display or manipulating server-side data, mastering multilevel sorting in JavaScript can be valuable. We’ll explore sorting basics, advance to multilevel sorting, and culminate with a dynamic, database-like solution, equipping you to handle complex sorting scenarios efficiently.

How array sorting works in JavaScript – a short recap

Understanding the Array.prototype.sort() method

The built-in Array.prototype.sort() function is pivotal for array sorting. It operates by accepting a comparison function that evaluates array elements in pairs, determining their order based on the returned numeric value:

  • Negative Return (< 0): a should be placed before b.
  • Positive Return (> 0): a should come after b.
  • Zero Return (0): The original order of a and b should be preserved.

This method alters the original array and returns a reference to it. Without a comparison function, elements are converted to strings and compared based on UTF-16 code units, often leading to unexpected results. Thus, providing a comparison function is crucial for predictable sorting, like the value "10" coming before "2" because "10" is ordered before "2" as strings.

Examples of sorting

1) Numerical Sorting:

For numerical sorting, subtracting one element from another is straightforward and effective:

const array = [1, 3, 2, 10, 30, 20];

array.sort(); // Returns [1, 10, 2, 20, 3, 30]
array.sort((a, b) => a - b); // Returns [1, 2, 3, 10, 20, 30]
array.sort((a, b) => b - a); // Returns [30, 20, 10, 3, 2, 1]

2) Sorting Objects:

More complex sorting, like sorting objects, requires more advanced comparison functions.

const cities = [{"city": "New York"}, {"city": "Chicago"}, {"city": "San Francisco"}];

cities.sort((a, b) => {
  if (a.city > b.city) return 1;
  if (a.city < b.city) return -1;
  return 0;
});
// Returns [
//  {"city": "Chicago"}, 
//  {"city": "New York"},
//  {"city": "San Francisco"}
// ]

3) Descending string sorting:

For descending order, the comparison function should reflect the reverse order:

const fruits = ["Banana", "Orange", "Apple", "Mango"];

fruits.sort();
// Returns ["Apple", "Banana", "Mango", "Orange"]

fruits.sort((a, b) => a > b ? -1 : a < b ? 1 : 0); 
// Returns ["Orange", "Mango", "Banana", "Apple"]

4) Using a toString() method for comparison:

Defining a toString() method in objects enables direct comparability, though it’s a less common approach:

class Person {
  constructor(city, age) {
    this.city = city;
    this.age = age;
  }
 
  toString() {
    return `${this.city}-${this.age}`;
  }
}

const person1 = new Person("New York", 15);
const person2 = new Person("Los Angeles", 18);
const person3 = new Person("Los Angeles", 17);

// Sorting based on the string representation provided by toString()
[person1, person2, person3].sort();

// Returns [
//   Person { city: 'Los Angeles', age: 17 },
//   Person { city: 'Los Angeles', age: 18 },
//   Person { city: 'New York', age: 15 }
// ]

In the next section, we’ll explore how to extend these principles to implement multilevel sorting in JavaScript.

Introduction to multilevel sorting in JavaScript

Multilevel sorting becomes essential when more than sorting by a single criterion is needed to organize data comprehensively. This technique involves considering additional sorting criteria when earlier levels are equal, akin to SQL’s ORDER BY functionality.

Implementing in JavaScript

In JavaScript, multilevel sorting can be achieved by extending the sort() function’s comparison logic to include multiple criteria using chained comparisons:

const data = [
  { name: "Alice", age: 22 }  
  { name: "Bob", age: 25 },
  { name: "Alice", age: 30 },
];

data.sort((a, b) => {
  return a.name.localeCompare(b.name) || b.age - a.age;
});
// Returns [
//   { name: 'Alice', age: 30 },
//   { name: 'Alice', age: 22 },
//   { name: 'Bob', age: 25 }
// ]

Here, the localeCompare method sorts names alphabetically, while ages are sorted numerically in descending order. This approach allows for seamless transitions between sorting criteria.

Advanced Multilevel Sorting with Predefined Functions

While starting with inline comparisons offers clarity, real-world applications demand flexibility. By defining functions like sortByClassAsc, sortByGradeDesc, and sortByAgeDesc, and then combining them with a reducer, we can create a powerful, composite sorting function.

function sortByClassAsc(a, b) {
  return a.class.localeCompare(b.class);
}

function sortByGradeDesc(a, b) {
  const gradeValues = { 'E': 4, 'S': 3, 'N': 2, 'U': 1 };
  return gradeValues[b.grade] - gradeValues[a.grade];
}

function sortByAgeDesc(a, b) {
  return b.age - a.age;
}

Next, let’s combine these functions using a reducer approach to create a single comparison function:

function combineComparisonFunctions(compareFunctions) {
  return (a, b) => {
    return compareFunctions.reduce((result, compareFunction) => {
      return result || compareFunction(a, b);
    }, 0);
  };
}

const combinedCompareFunction = combineComparisonFunctions([
  sortByClassAsc, 
  sortByGradeDesc, 
  sortByAgeDesc
]);

With this combined function, we can now effectively sort a list of students, for example:

const students = [
  { name: "Anna", class: '5A', grade: 'E', age: 10 },
  { name: "Lucas", class: '5A', grade: 'N', age: 12 },
  // ...Other students
];

students.sort(combinedCompareFunction);
// Result: Students sorted by class, grade (descending), and age (descending)

Next, we’ll generalize this solution to use dynamically-defined comparison functions instead of predefined ones. That will help us improve our code, making it more readable and maintainable.

Generalizing Multilevel Sorting

As we’ve navigated through the nuances of sorting arrays in JavaScript, from basic to multilevel sorting, a clear need emerges for a dynamic, adaptable approach. This need is especially pronounced when developing scalable applications across a broad spectrum of data types and sorting requirements. Our solution? The multilevelSort function. This innovative function empowers developers to define sorting criteria dynamically, encompassing each criterion’s property, desired sort direction, and an optional transformation function for unparalleled flexibility.

The multilevelSort Function Explained

At the heart of our generalized sorting approach is the multilevelSort function:

function multilevelSort(criteria) {
  const compareFunctions = criteria.map(
    ({ property, direction, transform = (x) => x }) => {
      return (a, b) => {
        const aValue = transform(a[property]);
        const bValue = transform(b[property]);
        if (aValue < bValue) return direction === 'asc' ? -1 : 1;
        if (aValue > bValue) return direction === 'asc' ? 1 : -1;
        return 0; // Elements are equal
      };
    },
  );

  return combineComparisonFunctions(compareFunctions);
}

This function ingeniously transforms an array of sorting criteria into a sequence of comparison functions. Each function is tailored to sort by a specific property, in a specified direction, and applies an optional transformation to property values, ensuring a precise, custom fit for any sorting challenge.

Applying multilevelSort

Embrace the power of multilevelSort to meet your specific data sorting needs:

const sortedStudents = students.sort(multilevelSort([
  { property: 'class', direction: 'asc' },
  { property: 'grade', direction: 'desc', transform: transformGrade },
  { property: 'age', direction: 'desc' }
]));

Here, transformGrade maps letter grades to numerical values, facilitating a natural, intuitive sorting process from excellent to unsatisfactory grades, then by age within each grade level.

Benefits of Generalized Multilevel Sorting

The multilevelSort function stands as a testament to the adaptability and power of JavaScript for data sorting, offering:

  • Versatility across diverse data structures and sorting needs.
  • Simplified complexity, condensing intricate sorting logic into a clear, declarative format.
  • Enhanced readability and maintainability, abstracting the comparison logic to focus on what truly matters: the data and its meaningful presentation.

This generalized approach not only meets the evolving requirements of modern applications but ensures data is always sorted in the most effective, user-centric manner.

Putting It All Together

Illustrating the culmination of our discussion, let’s apply the multilevelSort function to a practical scenario: sorting a dataset of students by class, grade, and age. This real-world application underscores the function’s versatility and highlights its capacity to efficiently organize complex data sets.

function combineComparisonFunctions(compareFunctions) {
  return (a, b) => {
    return compareFunctions.reduce((result, compareFunction) => {
      // Proceeds to the next comparison function if the previous one returned 0
      return result || compareFunction(a, b);
    }, 0);
  };
}

function multilevelSort(criteria) {
  const compareFunctions = criteria.map(
    ({ property, direction, transform = (x) => x }) => {
      return (a, b) => {
        const aValue = transform(a[property]); // optionally transform the property value
        const bValue = transform(b[property]); // we use an identity function as default
        if (aValue < bValue) return direction === 'asc' ? -1 : 1;
        if (aValue > bValue) return direction === 'asc' ? 1 : -1;
        return 0;
      };
    },
  );

  return combineComparisonFunctions(compareFunctions);
}

const students = [
  { name: 'Anna', class: '5A', grade: 'E', age: 10 },
  { name: 'Lucas', class: '5A', grade: 'N', age: 12 },
  { name: 'Peter', class: '5B', grade: 'S', age: 11 },
  { name: 'John', class: '5A', grade: 'S', age: 10 },
  { name: 'Alice', class: '5C', grade: 'U', age: 13 },
  { name: 'Clair', class: '5B', grade: 'E', age: 12 },
  { name: 'William', class: '5C', grade: 'N', age: 11 },
  { name: 'Beatrice', class: '5A', grade: 'E', age: 11 },
  { name: 'Sophie', class: '5B', grade: 'N', age: 12 },
  { name: 'Matthew', class: '5C', grade: 'S', age: 10 },
];

const gradeMapping = { E: 4, S: 3, N: 2, U: 1 };

function transformGrade(grade) {
  return gradeMapping[grade] || 0;
}

const sortedStudents = students.sort(
  multilevelSort([
    { property: 'class', direction: 'asc' },
    { property: 'grade', direction: 'desc', transform: transformGrade },
    { property: 'age', direction: 'desc' },
  ]),
);

console.table(sortedStudents);

The resulting table exemplifies the algorithm’s efficacy, demonstrating a clear, actionable method for sorting data across multiple dimensions.

IndexNameClassGradeAge
0Beatrice5AE11
1Anna5AE10
2John5AS10
3Lucas5AN12
4Clair5BE12
5Peter5BS11
6Sophie5BN12
7Matthew5CS10
8William5CN11
9Alice5CU13
The array of Students sorted by Class, Grade (descending), and Age (descending)

This practical example not only brings our theoretical discussion to life but also showcases the direct applicability and impact of these advanced sorting techniques in real-world scenarios.

Conclusion

Through this exploration of JavaScript’s sorting capabilities, from single-level to dynamic multilevel sorting, we’ve unveiled the language’s remarkable flexibility and power in handling complex data structures. By mastering these advanced techniques, developers can significantly enhance the data processing capabilities of their applications, ensuring that even the most complex datasets are sorted efficiently and effectively to meet precise user needs and preferences.

The journey from basic sorting principles to the implementation of a comprehensive, dynamic sorting solution encapsulates the essence of modern software development: adaptability, precision, and user focus. Armed with these techniques, you’re now equipped to tackle a wide array of data sorting challenges, paving the way for clearer data presentation and improved user experiences in your JavaScript applications.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *