51

I have this function to sort a JavaScript array of objects based on a property:

// arr is the array of objects, prop is the property to sort by
var sort = function (prop, arr) {
    arr.sort(function (a, b) {
        if (a[prop] < b[prop]) {
            return -1;
        } else if (a[prop] > b[prop]) {
            return 1;
        } else {
            return 0;
        }
    });
};

It works with arrays like this:

sort('property', [
    {property:'1'},
    {property:'3'},
    {property:'2'},
    {property:'4'},
]);

But I want to be able to sort also by nested properties, for example something like:

sort('nestedobj.property', [
    {nestedobj:{property:'1'}},
    {nestedobj:{property:'3'}},
    {nestedobj:{property:'2'}},
    {nestedobj:{property:'4'}}
]);

However this doesn't work because it is not possible to do something like object['nestedobj.property'], it should be object['nestedobj']['property'].

Do you know how could I solve this problem and make my function work with properties of nested objects?

Thanks in advance

VerizonW
  • 1,785
  • 5
  • 21
  • 27

13 Answers13

44

You can split the prop on ., and iterate over the Array updating the a and b with the next nested property during each iteration.

Example: http://jsfiddle.net/x8KD6/1/

var sort = function (prop, arr) {
    prop = prop.split('.');
    var len = prop.length;

    arr.sort(function (a, b) {
        var i = 0;
        while( i < len ) { a = a[prop[i]]; b = b[prop[i]]; i++; }
        if (a < b) {
            return -1;
        } else if (a > b) {
            return 1;
        } else {
            return 0;
        }
    });
    return arr;
};
user113716
  • 318,772
  • 63
  • 451
  • 440
  • Thanks this worked and was faster than the other options suggested. – VerizonW Feb 24 '11 at 19:12
  • @VerizonW: Glad it worked. Yeah, I like to avoid function calls when I'm able. Keeps things snappy. :o) – user113716 Feb 24 '11 at 19:14
  • This doesn't seem to work with property values that are different lengths. E.g. `{nestedobj:{property:'g'}}, {nestedobj:{property:'F'}}, {nestedobj:{property:'abcd'}}, {nestedobj:{property:'abba'}}` – Alan Wells May 24 '18 at 16:01
13

Use Array.prototype.sort() with a custom compare function to do the descending sort first:

champions.sort(function(a, b) { return b.level - a.level }).slice(...

Even nicer with ES6:

champions.sort((a, b) => b.level - a.level).slice(...
rat
  • 1,277
  • 16
  • 24
8

Instead of passing the property as a string, pass a function that can retrieve the property from the top level object.

var sort = function (propertyRetriever, arr) {
    arr.sort(function (a, b) {
        var valueA = propertyRetriever(a);
        var valueB = propertyRetriever(b);

        if (valueA < valueB) {
            return -1;
        } else if (valueA > valueB) {
            return 1;
        } else {
            return 0;
        }
    });
};

Invoke as,

var simplePropertyRetriever = function(obj) {
    return obj.property;
};

sort(simplePropertyRetriever, { .. });

Or using a nested object,

var nestedPropertyRetriever = function(obj) {
    return obj.nestedObj.property;
};

sort(nestedPropertyRetriever, { .. });
Anurag
  • 140,337
  • 36
  • 221
  • 257
7

if you have array of objects like

const objs = [{
        first_nom: 'Lazslo',
        last_nom: 'Jamf',
        moreDetails: {
            age: 20
        }
    }, {
        first_nom: 'Pig',
        last_nom: 'Bodine',
        moreDetails: {
            age: 21
        }
    }, {
        first_nom: 'Pirate',
        last_nom: 'Prentice',
        moreDetails: {
            age: 22
        }
    }];

you can use simply

nestedSort = (prop1, prop2 = null, direction = 'asc') => (e1, e2) => {
        const a = prop2 ? e1[prop1][prop2] : e1[prop1],
            b = prop2 ? e2[prop1][prop2] : e2[prop1],
            sortOrder = direction === "asc" ? 1 : -1
        return (a < b) ? -sortOrder : (a > b) ? sortOrder : 0;
    }

and call it

for direct objects

objs.sort(nestedSort("last_nom"));
objs.sort(nestedSort("last_nom", null, "desc"));

for nested objects

objs.sort(nestedSort("moreDetails", "age"));
objs.sort(nestedSort("moreDetails", "age", "desc"));
Mas
  • 1,267
  • 1
  • 14
  • 13
  • Out of all these solutions I used yours and it works great. I did have to change the code to add a 3rd level deep. – howserss Aug 15 '22 at 19:23
  • 1
    This is what I did -> const a = prop3 ? e1[prop1][prop2][prop3] : prop2 ? e1[prop1][prop2] : e1[prop1], b = prop3 ? e2[prop1][prop2][prop3] : prop2 ? e2[prop1][prop2] : e2[prop1], sortOrder = direction === 'asc' ? 1 : -1; – howserss Aug 15 '22 at 19:27
  • Thank you for providing a generic solution. I am using this now. But I always find difficult to understand code written in currying format. Is there any other way to achieve same. @Mas – Swapnil Mhaske Aug 30 '22 at 08:56
  • @SwapnilMhaske you may use https://es6console.com/ to convert es6 to es5 code, it may be like: nestedSort = function nestedSort(prop1) { var prop2 = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1]; var direction = arguments.length <= 2 || arguments[2] === undefined ? 'asc' : arguments[2]; return function (e1, e2) { var a = prop2 ? e1[prop1][prop2] : e1[prop1], b = prop2 ? e2[prop1][prop2] : e2[prop1], sortOrder = direction === "asc" ? 1 : -1; return a < b ? -sortOrder : a > b ? sortOrder : 0; }; }; – Mas Sep 01 '22 at 18:17
4

You can use Agile.js for this kind of things.
Actually you pass an expression instead of callback, it's handle nested properties and javascript expression in a very nice-ish way.

Usage: _.orderBy(array, expression/callback, reverse[optional])

Example:

var orders = [
  { product: { price: 91.12, id: 1 }, date: new Date('01/01/2014') },
  { product: { price: 79.21, id: 2 }, date: new Date('01/01/2014') },
  { product: { price: 99.90, id: 3 }, date: new Date('01/01/2013') },
  { product: { price: 19.99, id: 4 }, date: new Date('01/01/1970') }
];

_.orderBy(orders, 'product.price');
// →  [orders[3], orders[1], orders[0], orders[2]]

_.orderBy(orders, '-product.price');
// → [orders[2], orders[0], orders[1], orders[3]]
a8m
  • 9,334
  • 4
  • 37
  • 40
1

Would this meet your needs?

// arr is the array of objects, prop is the property to sort by
var sort = function (nestedObj, prop, arr) {
    arr.sort(function (a, b) {
        if (a[nestedObj][prop] < b[nestedObj][prop]) {
            return -1;
        } else if (a[nestedObj][prop] > b[nestedObj][prop]) {
            return 1;
        } else {
            return 0;
        }
    });
};
jessegavin
  • 74,067
  • 28
  • 136
  • 164
0

Try this (used a recursive function to get nested value, you can pass the nested property as nestedobj.property): You can use this for any level of hierarchy

// arr is the array of objects, prop is the property to sort by
var getProperty = function(obj, propNested){
 if(!obj || !propNested){
  return null;
 }
 else if(propNested.length == 1) {
    var key = propNested[0];
    return obj[key];
 }
 else {
  var newObj = propNested.shift();
    return getProperty(obj[newObj], propNested);
 }
};
var sort = function (prop, arr) {
    arr.sort(function (a, b) {
                var aProp = getProperty(a, prop.split("."));
                var bProp = getProperty(a, prop.split("."));
        if (aProp < bProp) {
            return -1;
        } else if (aProp > bProp) {
            return 1;
        } else {
            return 0;
        }
    });
};
Chandu
  • 81,493
  • 19
  • 133
  • 134
0

This is my modify code.

// arr is the array of objects, prop is the property to sort by
var s = function (prop, arr) {
    // add sub function for get value from obj (1/2)
    var _getVal = function(o, key){
        var v = o;
        var k = key.split(".");
        for(var i in k){
            v = v[k[i]];
        }
        return v;
    }
    return arr.sort(function (a, b) {
        // get value from obj a, b before sort (2/2)
        var aVal = _getVal(a, prop);
        var bVal = _getVal(b, prop);
        if (aVal < bVal) {
            return -1;
        } else if (aVal > bVal) {
            return 1;
        } else {
            return 0;
        }
    });
};
diewland
  • 1,865
  • 5
  • 30
  • 41
0

var objectsArr = [
  {nestedobj:{property:'1'}},
  {nestedobj:{property:'3'}},
  {nestedobj:{property:'2'}},
  {nestedobj:{property:'4'}}
];

function getFromPath(obj, path) {
  let r = obj;
  path.forEach(key => { r = r[key]})
  return r
}

function sortObjectsArr(objectsArray, ...path) {
  objectsArray.sort((a, b) => getFromPath(a, path) - getFromPath(b, path))
}

sortObjectsArr(objectsArr, 'nestedobj', 'property');

console.log(objectsArr);

Unfortunately, I didn't find any nice way to use the arguments in order to access the attributes of the nested object.
Want to mention that there can be some checks if the keys are available in the passed object, but this depends on who and how want to implement this.

Philipos D.
  • 2,036
  • 1
  • 26
  • 33
0

Description

My solution is this one. I decide to flat the object first:

function flattenObject(value: any): any {
    let toReturn: any = {};

    for (const i in value) {
        if (!value.hasOwnProperty(i)) {
            continue;
        }

        if (typeof value[i] == 'object') {
            const flatObject = flattenObject(value[i]);
            for (const x in flatObject) {
                if (!flatObject.hasOwnProperty(x)) continue;

                toReturn[i + '.' + x] = flatObject[x];
            }
        } else {
            toReturn[i] = value[i];
        }
    }
    return toReturn;
}

And then I'll extract the value from the object:

function nestedFieldValue(
    nestedJoinedFieldByDot: string,
    obj: any,
): any {
    return flattenObject(obj)[nestedJoinedFieldByDot];
}

Ant at the end I just need to do this:

export function fieldSorter(fields: string[]) {
    return function (a: any, b: any) {
        return fields
            .map(function (fieldKey) {
                // README: Sort Ascending by default
                let dir = 1;

                if (fieldKey[0] === '-') {
                    // README: Sort Descending if `-` was passed at the beginning of the field name
                    dir = -1;
                    fieldKey = fieldKey.substring(1);
                }

                const aValue = nestedFlattenObjectFieldValue(
                    fieldKey,
                    a,
                );
                const bValue = nestedFlattenObjectFieldValue(
                    fieldKey,
                    b,
                );

                if (
                    typeof aValue === 'number' ||
                    typeof bValue === 'number'
                ) {
                    /**
                     * README: default value when the field does not exists to prevent unsorted array
                     * I assume that 0 should be the last element. In other word I sort arrays in a way
                     * that biggest numbers comes first and then smallest numbers
                     */
                    if (aValue ?? 0 > bValue ?? 0) {
                        return dir;
                    }
                    if (aValue ?? 0 < bValue ?? 0) {
                        return -dir;
                    }
                } else {
                    if (aValue ?? 0 > bValue ?? 0) {
                        return dir;
                    }
                    if (aValue ?? 0 < bValue ?? 0) {
                        return -dir;
                    }
                }

                return 0;
            })
            .reduce(function firstNonZeroValue(p, n) {
                return p ? p : n;
            }, 0);
    };
}

Finally we need to do this:

const unsorted = [ 
    { 
        city: { 
            priority: 1, 
            name: 'Tokyo', 
            airport: { name: 'Haneda Airport' } 
        }
    }
]

const result = unsorted.sort(
    fieldSorter(['city.priority', 'city.airport.name', 'city.name']),
);

I think this way is much much clear and cleaner. It is readable and more functional. I merge multiple answer from stackoverflow to reach this solution :sweat_smile:

Kasir Barati
  • 606
  • 13
  • 24
0

This should work and can accept multiple parameters.

https://codepen.io/allenACR/pen/VwQKWZG

function sortArrayOfObjects(items, getter) {
  const copy = JSON.parse(JSON.stringify(items));

  const sortFn = fn => {
    copy.sort((a, b) => {
      a = fn(a)
      b = fn(b)
      return a === b ? 0 : a < b ? -1 : 1;
    });
  };

  getter.forEach(x => {
    const fn = typeof x === 'function' ? x : item => item[x];
    sortFn(fn);
  });

  return copy;
}

// example dataset
const data = [
  {id: 3, name: "Dave", details: {skill: "leader"} },
  {id: 1, name: "Razor", details: {skill: "music"} }, 
  {id: 2, name: "Syd", details: {skill: "animal husbandry"} }
]

// sort via single prop
const sort1 = sortArrayOfObjects(data, ["id"])
// returns [Razor, Syd, Dave]

// sort via nested
const sort2 = sortArrayOfObjects(data, [
  (item) => item.details.skill  
])
// returns [Syd, Dave, Razor]

console.log({sort1, sort2})
0

3 levels deep. path can look like this. 'level1' or 'level1.level2' or 'level1.level2.level3' I also did uppercase for the sort all my items are strings. Anwser is a modified version from - @Mas

public keysrt(arr: Object[], path: string, reverse: boolean): void {
    const nextOrder = reverse ? -1 : 1;
    const pathSplit = path.split('.');
    if (arr === null || arr === undefined ) {
        return;
    }
    if (arr.length <= 1) {
        return;
    }

    const nestedSort = (prop1, prop2 = null, prop3 = null, direction = 'asc') => (e1, e2) => {
        const a = prop3 ? e1[prop1][prop2][prop3] : prop2 ? e1[prop1][prop2] : e1[prop1],
            b = prop3 ? e2[prop1][prop2][prop3] : prop2 ? e2[prop1][prop2] : e2[prop1],
            sortOrder = direction === 'asc' ? 1 : -1;
        return (a.toString().toUpperCase() < b.toString().toUpperCase()) ?
            -sortOrder * nextOrder : (a.toString().toUpperCase() > b.toString().toUpperCase()) ?
                sortOrder * nextOrder : 0;
    };

    if (pathSplit.length === 3) {
        arr.sort(nestedSort(pathSplit[0], pathSplit[1], pathSplit[2]));
    }
    if (pathSplit.length === 2) {
        arr.sort(nestedSort(pathSplit[0], pathSplit[1]));
    }
    if (pathSplit.length === 1) {
        arr.sort(nestedSort(pathSplit[0], null));
    }
}
howserss
  • 1,139
  • 1
  • 8
  • 12
-1

For those who a researching on how to sort nested properties. I created a small type-safe array sorting method with support for deeply nested properties and Typescript autocompletion.

https://github.com/jvandenaardweg/sort-by-property

https://www.npmjs.com/package/sort-by-property

Example:

blogPosts.sort(sortByProperty('author.name', 'asc'));