Search code examples
javascriptsortingarray-multisort

How can I "multi sort" an array of objects in Javascript by keys


I'm trying to "enhance" my sorting function for accepting multiple criteria and so doing multi sorting of my array of objects.

Sorry if this is a simple question, or if I committed "newbie" mistakes, but I'm a Frontend developer learning JS foundamentals, and I'm trying to learn how sorting functions working.

Here's my code:

const arrayOfObjects = [
    {
        name: 'John',
        age: 30,
        city: 'New York',
        details: {
            occupation: 'developer',
        }
    },
    {
        name: 'Jane',
        age: 25,
        city: 'Paris',
        details : {
            occupation: 'designer',
        }
    }
];

// This is my main sorting function that I'd like to use for "multi sorting" an array of objects
function sortByKey(criteria) {
            
    return (a, b) => {
        let comparison = 0;

        criteria.forEach(criterion => {
            const varA = (typeof resolvePath(criterion.key, a) === 'string')
            ? resolvePath(criterion.key, a).toUpperCase() : resolvePath(criterion.key, a);
            const varB = (typeof resolvePath(criterion.key, b) === 'string')
            ? resolvePath(criterion.key, b).toUpperCase() : resolvePath(criterion.key, b);

            if (varA > varB) {
                comparison = 1;
            } else if (varA < varB) {
                comparison = -1;
            }

            if(criterion.order === 'desc') {
                comparison = (comparison * -1)
            } 
        });
        
        return comparison;
    };
}

// This function is used to resolve the path of a key in an object
function resolvePath(path, obj) {
    return path.split('.').reduce(function(prev, curr) {
        return prev ? prev[curr] : null
    }, obj || self)
}

The usage of this is:

// MAIN USAGE
console.log( arrayOfObjects.sort(sortByKey([ { key: 'age' } ])) )
console.log( arrayOfObjects.sort(sortByKey([ { key: 'details.occupation', order: 'desc' } ])) )

But I would like to use it some kind of:

console.log( arrayOfObjects.sort(sortByKey([ { key: 'details.occupation' }, { key: 'age', order: 'desc' ])) )

This is because I want to give users the possibility to multi-sort my array in some kind of order, like "Age first, name second, etc..."

Thanks for any useful help, have a nice day!


Solution

  • Create a function which returns the comparator value for each criteria. Loop through the array and call the compare function UNTIL a non-zero value is found. find will stop looking when compareByKey returns 1 or -1.

    const sortByKey = (criteria) => (a, b) => {
      let returnValue = 0;
      criteria.find(c => returnValue = compareByKey(c, a, b));
      return returnValue;
    }
    
    function compareByKey({ key, order = 'asc' }, a, b) {
      const varA = resolvePath(key, a)
      const varB = resolvePath(key, b)
    
      let comparison = 0;
    
      if (varA > varB) {
        comparison = 1;
      } else if (varA < varB) {
        comparison = -1;
      }
    
      if (order === 'desc') {
        comparison *= -1
      }
      
      return comparison
    }
    

    If the find part is confusing or ugly:

    Another way would be to reduce the comparator value of each property. But, this doesn't short circuit when a non-zero value is found.

    const sortByKey = criteria => 
                        (a, b) => 
                          criteria.reduce((acc,c) => acc || compareByKey(c, a, b), 0)
    

    or

    const sortByKey = (criteria) => (a, b) => {
      let comparedValue = 0;
    
      for (const c of criteria) {
        comparedValue = compareByKey(c, a, b);
    
        if (comparedValue)
          return comparedValue;
      }
      
      return comparedValue;
    }
    

    const arrayOfObjects = [{
        name: 'John',
        age: 30,
        city: 'New York',
        details: {
          occupation: 'developer',
        }
      },
      {
        name: 'Jane',
        age: 25,
        city: 'Paris',
        details: {
          occupation: 'designer',
        }
      },
      // added another designer for testing
      {
        name: 'Jane 2',
        age: 30,
        city: 'Paris',
        details: {
          occupation: 'designer',
        }
      }
    ];
    
    const sortByKey = (criteria) => (a, b) => {
      let returnValue = 0;
      criteria.find(c => returnValue = compareByKey(c, a, b));
      return returnValue;
    }
    
    function compareByKey({ key, order = 'asc' }, a, b) {
      const varA = resolvePath(key, a)
      const varB = resolvePath(key, b)
    
      let comparison = 0;
    
      if (varA > varB) {
        comparison = 1;
      } else if (varA < varB) {
        comparison = -1;
      }
    
      if (order === 'desc') {
        comparison *= -1
      }
      
      return comparison
    }
    
    // This function is used to resolve the path of a key in an object
    function resolvePath(path, obj) {
      return path.split('.').reduce(function(prev, curr) {
        return prev ? prev[curr] : null
      }, obj || self)
    }
    
    console.log(arrayOfObjects.sort(sortByKey([
      { key: 'details.occupation' }, 
      { key: 'age', order: 'desc' }
    ])))