JS: How to sort on multiple properties? [duplicate] - javascript

From this original question, how would I apply a sort on multiple fields?
Using this slightly adapted structure, how would I sort city (ascending) & then price (descending)?
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
I liked the fact than an answer was given which provided a general approach. Where I plan to use this code, I will have to sort dates as well as other things. The ability to "prime" the object seemed handy, if not a little cumbersome.
I've tried to build this answer into a nice generic example, but I'm not having much luck.

You could use a chained sorting approach by taking the delta of values until it reaches a value not equal to zero.
var data = [{ h_id: "3", city: "Dallas", state: "TX", zip: "75201", price: "162500" }, { h_id: "4", city: "Bevery Hills", state: "CA", zip: "90210", price: "319250" }, { h_id: "6", city: "Dallas", state: "TX", zip: "75000", price: "556699" }, { h_id: "5", city: "New York", state: "NY", zip: "00010", price: "962500" }];
data.sort(function (a, b) {
return a.city.localeCompare(b.city) || b.price - a.price;
});
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Or, using es6, simply:
data.sort((a, b) => a.city.localeCompare(b.city) || b.price - a.price);

for a non-generic, simple solution to your exact problem:
homes.sort(
function(a, b) {
if (a.city === b.city) {
// Price is only important when cities are the same
return b.price - a.price;
}
return a.city > b.city ? 1 : -1;
});

A multi dimensional sorting method, based on this answer:
Update: Here is an "optimized" version. It does a lot more preprocessing and creates a comparison function for each sorting option beforehand. It might need more more memory (as it stores a function for each sorting option, but it should preform a bit better as it does not have to determine the correct settings during the comparison. I have not done any profiling though.
var sort_by;
(function() {
// utility functions
var default_cmp = function(a, b) {
if (a == b) return 0;
return a < b ? -1 : 1;
},
getCmpFunc = function(primer, reverse) {
var dfc = default_cmp, // closer in scope
cmp = default_cmp;
if (primer) {
cmp = function(a, b) {
return dfc(primer(a), primer(b));
};
}
if (reverse) {
return function(a, b) {
return -1 * cmp(a, b);
};
}
return cmp;
};
// actual implementation
sort_by = function() {
var fields = [],
n_fields = arguments.length,
field, name, reverse, cmp;
// preprocess sorting options
for (var i = 0; i < n_fields; i++) {
field = arguments[i];
if (typeof field === 'string') {
name = field;
cmp = default_cmp;
}
else {
name = field.name;
cmp = getCmpFunc(field.primer, field.reverse);
}
fields.push({
name: name,
cmp: cmp
});
}
// final comparison function
return function(A, B) {
var a, b, name, result;
for (var i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
name = field.name;
result = field.cmp(A[name], B[name]);
if (result !== 0) break;
}
return result;
}
}
}());
Example usage:
homes.sort(sort_by('city', {name:'price', primer: parseInt, reverse: true}));
DEMO
Original function:
var sort_by = function() {
var fields = [].slice.call(arguments),
n_fields = fields.length;
return function(A,B) {
var a, b, field, key, primer, reverse, result, i;
for(i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
key = typeof field === 'string' ? field : field.name;
a = A[key];
b = B[key];
if (typeof field.primer !== 'undefined'){
a = field.primer(a);
b = field.primer(b);
}
reverse = (field.reverse) ? -1 : 1;
if (a<b) result = reverse * -1;
if (a>b) result = reverse * 1;
if(result !== 0) break;
}
return result;
}
};
DEMO

Here is a simple functional generic approach. Specify sort order using array. Prepend minus to specify descending order.
var homes = [
{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":"319250"},
{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":"556699"},
{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":"962500"}
];
homes.sort(fieldSorter(['city', '-price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function (a, b) {
return fields
.map(function (o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o=o.substring(1);
}
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
})
.reduce(function firstNonZeroValue (p,n) {
return p ? p : n;
}, 0);
};
}
Edit: in ES6 it's even shorter!
"use strict";
const fieldSorter = (fields) => (a, b) => fields.map(o => {
let dir = 1;
if (o[0] === '-') { dir = -1; o=o.substring(1); }
return a[o] > b[o] ? dir : a[o] < b[o] ? -(dir) : 0;
}).reduce((p, n) => p ? p : n, 0);
const homes = [{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":162500}, {"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":319250},{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":556699},{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":962500}];
const sortedHomes = homes.sort(fieldSorter(['state', '-price']));
document.write('<pre>' + JSON.stringify(sortedHomes, null, '\t') + '</pre>')

I made a quite generic multi feature sorter today. You can have a look at thenBy.js here: https://github.com/Teun/thenBy.js
It allows you to use the standard Array.sort, but with firstBy().thenBy().thenBy() style. It is way less code and complexity than the solutions posted above.

Here's an extensible way to sort by multiple fields.
homes.sort(function(left, right) {
var city_order = left.city.localeCompare(right.city);
var price_order = parseInt(left.price) - parseInt(right.price);
return city_order || -price_order;
});
Notes
A function passed to array sort is expected to return negative, zero, positive to indicate less, equal, greater.
a.localeCompare(b) is universally supported for strings, and returns -1,0,1 if a<b,a==b,a>b.
Subtraction works on numeric fields, because a - b gives -,0,+ if a<b,a==b,a>b.
|| in the last line gives city priority over price.
Negate to reverse order in any field, as in -price_order
Add new fields to the or-chain: return city_order || -price_order || date_order;
Date compare with subtraction, because date math converts to milliseconds since 1970.var date_order = new Date(left.date) - new Date(right.date);
Boolean compare with subtraction, which is guaranteed to turn true and false to 1 and 0 (therefore the subtraction produces -1 or 0 or 1). var goodness_order = Boolean(left.is_good) - Boolean(right.is_good)This is unusual enough that I'd suggest drawing attention with the Boolean constructor, even if they're already boolean.

This is a complete cheat but I think that it adds value to this question because it's basically a canned library function that you can use out-of-the box.
If your code has access to lodash or a lodash compatible library like underscore then you can use the _.sortBy method. The snippet below is copied directly from the lodash documentation.
The commented results in the examples looks like they return arrays of arrays but that's just showing the order and not the actual results which are an array of objects.
var users = [
{ 'user': 'fred',   'age': 48 },
{ 'user': 'barney', 'age': 36 },
{ 'user': 'fred',   'age': 40 },
{ 'user': 'barney', 'age': 34 }
];
_.sortBy(users, [function(o) { return o.user; }]);
// => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]
_.sortBy(users, ['user', 'age']);
// => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]]

The following function will allow you to sort an array of objects on one or multiple properties, either ascending (default) or descending on each property, and allow you to choose whether or not to perform case sensitive comparisons. By default, this function performs case insensitive sorts.
The first argument must be the array containing the objects.
The subsequent argument(s) must be a comma separated list of strings that reference the different object properties to sort by. The last argument (which is optional) is a boolean to choose whether or not to perform case sensitive sorts - use true for case sensitive sorts.
The function will sort each property/key in ascending order by default. If you want a particular key to sort in descending order, then instead pass in an array in this format: ['property_name', true].
Here are some sample uses of the function followed by an explanation (where homes is an array containing the objects):
objSort(homes, 'city') --> sort by city (ascending, case in-sensitive)
objSort(homes, ['city', true]) --> sort by city (descending, case in-sensitive)
objSort(homes, 'city', true) --> sort by city then price (ascending, case sensitive)
objSort(homes, 'city', 'price') --> sort by city then price (both ascending, case in-sensitive)
objSort(homes, 'city', ['price', true]) --> sort by city (ascending) then price (descending), case in-sensitive)
And without further ado, here's the function:
function objSort() {
var args = arguments,
array = args[0],
case_sensitive, keys_length, key, desc, a, b, i;
if (typeof arguments[arguments.length - 1] === 'boolean') {
case_sensitive = arguments[arguments.length - 1];
keys_length = arguments.length - 1;
} else {
case_sensitive = false;
keys_length = arguments.length;
}
return array.sort(function (obj1, obj2) {
for (i = 1; i < keys_length; i++) {
key = args[i];
if (typeof key !== 'string') {
desc = key[1];
key = key[0];
a = obj1[args[i][0]];
b = obj2[args[i][0]];
} else {
desc = false;
a = obj1[args[i]];
b = obj2[args[i]];
}
if (case_sensitive === false && typeof a === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (! desc) {
if (a < b) return -1;
if (a > b) return 1;
} else {
if (a > b) return -1;
if (a < b) return 1;
}
}
return 0;
});
} //end of objSort() function
And here's some sample data:
var homes = [{
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": 162500
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": 1000000
}, {
"h_id": "5",
"city": "new york",
"state": "NY",
"zip": "00010",
"price": 1000000
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "85000",
"price": 300000
}, {
"h_id": "7",
"city": "New York",
"state": "NY",
"zip": "00020",
"price": 345000
}];

A dynamic way to do that with MULTIPLE keys:
filter unique values from each col/key of sort
put in order or reverse it
add weights width zeropad for each object based on indexOf(value) keys values
sort using caclutated weights
Object.defineProperty(Array.prototype, 'orderBy', {
value: function(sorts) {
sorts.map(sort => {
sort.uniques = Array.from(
new Set(this.map(obj => obj[sort.key]))
);
sort.uniques = sort.uniques.sort((a, b) => {
if (typeof a == 'string') {
return sort.inverse ? b.localeCompare(a) : a.localeCompare(b);
}
else if (typeof a == 'number') {
return sort.inverse ? b - a : a - b;
}
else if (typeof a == 'boolean') {
let x = sort.inverse ? (a === b) ? 0 : a? -1 : 1 : (a === b) ? 0 : a? 1 : -1;
return x;
}
return 0;
});
});
const weightOfObject = (obj) => {
let weight = "";
sorts.map(sort => {
let zeropad = `${sort.uniques.length}`.length;
weight += sort.uniques.indexOf(obj[sort.key]).toString().padStart(zeropad, '0');
});
//obj.weight = weight; // if you need to see weights
return weight;
}
this.sort((a, b) => {
return weightOfObject(a).localeCompare( weightOfObject(b) );
});
return this;
}
});
Use:
// works with string, number and boolean
let sortered = your_array.orderBy([
{key: "type", inverse: false},
{key: "title", inverse: false},
{key: "spot", inverse: false},
{key: "internal", inverse: true}
]);

Here's a generic multidimensional sort, allowing for reversing and/or mapping on each level.
Written in Typescript. For Javascript, check out this JSFiddle
The Code
type itemMap = (n: any) => any;
interface SortConfig<T> {
key: keyof T;
reverse?: boolean;
map?: itemMap;
}
export function byObjectValues<T extends object>(keys: ((keyof T) | SortConfig<T>)[]): (a: T, b: T) => 0 | 1 | -1 {
return function(a: T, b: T) {
const firstKey: keyof T | SortConfig<T> = keys[0];
const isSimple = typeof firstKey === 'string';
const key: keyof T = isSimple ? (firstKey as keyof T) : (firstKey as SortConfig<T>).key;
const reverse: boolean = isSimple ? false : !!(firstKey as SortConfig<T>).reverse;
const map: itemMap | null = isSimple ? null : (firstKey as SortConfig<T>).map || null;
const valA = map ? map(a[key]) : a[key];
const valB = map ? map(b[key]) : b[key];
if (valA === valB) {
if (keys.length === 1) {
return 0;
}
return byObjectValues<T>(keys.slice(1))(a, b);
}
if (reverse) {
return valA > valB ? -1 : 1;
}
return valA > valB ? 1 : -1;
};
}
Usage Examples
Sorting a people array by last name, then first name:
interface Person {
firstName: string;
lastName: string;
}
people.sort(byObjectValues<Person>(['lastName','firstName']));
Sort language codes by their name, not their language code (see map), then by descending version (see reverse).
interface Language {
code: string;
version: number;
}
// languageCodeToName(code) is defined elsewhere in code
languageCodes.sort(byObjectValues<Language>([
{
key: 'code',
map(code:string) => languageCodeToName(code),
},
{
key: 'version',
reverse: true,
}
]));

Here's another one that's perhaps closer to your idea for the syntax
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {}; // primers are optional
properties = properties.map(function(prop) {
if( !(prop instanceof Array) ) {
prop = [prop, 'asc']
}
if( prop[1].toLowerCase() == 'desc' ) {
prop[1] = -1;
} else {
prop[1] = 1;
}
return prop;
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}
// just for fun use this to reverse the city name when sorting
function demoPrimer(str) {
return str.split('').reverse().join('');
}
// Example
sortObjects(homes, ['city', ['price', 'desc']], {city: demoPrimer});
Demo: http://jsfiddle.net/Nq4dk/2/
Edit: Just for fun, here's a variation that just takes an sql-like string, so you can do sortObjects(homes, "city, price desc")
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {};
properties = properties.split(/\s*,\s*/).map(function(prop) {
prop = prop.match(/^([^\s]+)(\s*desc)?/i);
if( prop[2] && prop[2].toLowerCase() === 'desc' ) {
return [prop[1] , -1];
} else {
return [prop[1] , 1];
}
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}

Simpler one:
var someArray = [...];
function generateSortFn(props) {
return function (a, b) {
for (var i = 0; i < props.length; i++) {
var prop = props[i];
var name = prop.name;
var reverse = prop.reverse;
if (a[name] < b[name])
return reverse ? 1 : -1;
if (a[name] > b[name])
return reverse ? -1 : 1;
}
return 0;
};
};
someArray.sort(generateSortFn([{name: 'prop1', reverse: true}, {name: 'prop2'}]));

why complicate? just sort it twice! this works perfectly:
(just make sure to reverse the importance order from least to most):
jj.sort( (a, b) => (a.id >= b.id) ? 1 : -1 );
jj.sort( (a, b) => (a.status >= b.status) ? 1 : -1 );

I like SnowBurnt's approach but it needs a tweak to test for equivalence on city NOT a difference.
homes.sort(
function(a,b){
if (a.city==b.city){
return (b.price-a.price);
} else {
return (a.city-b.city);
}
});

Here's my solution based on the Schwartzian transform idiom, hope you find it useful.
function sortByAttribute(array, ...attrs) {
// generate an array of predicate-objects contains
// property getter, and descending indicator
let predicates = attrs.map(pred => {
let descending = pred.charAt(0) === '-' ? -1 : 1;
pred = pred.replace(/^-/, '');
return {
getter: o => o[pred],
descend: descending
};
});
// schwartzian transform idiom implementation. aka: "decorate-sort-undecorate"
return array.map(item => {
return {
src: item,
compareValues: predicates.map(predicate => predicate.getter(item))
};
})
.sort((o1, o2) => {
let i = -1, result = 0;
while (++i < predicates.length) {
if (o1.compareValues[i] < o2.compareValues[i]) result = -1;
if (o1.compareValues[i] > o2.compareValues[i]) result = 1;
if (result *= predicates[i].descend) break;
}
return result;
})
.map(item => item.src);
}
Here's an example how to use it:
let games = [
{ name: 'Pako', rating: 4.21 },
{ name: 'Hill Climb Racing', rating: 3.88 },
{ name: 'Angry Birds Space', rating: 3.88 },
{ name: 'Badland', rating: 4.33 }
];
// sort by one attribute
console.log(sortByAttribute(games, 'name'));
// sort by mupltiple attributes
console.log(sortByAttribute(games, '-rating', 'name'));

Just another option. Consider to use the following utility function:
/** Performs comparing of two items by specified properties
* #param {Array} props for sorting ['name'], ['value', 'city'], ['-date']
* to set descending order on object property just add '-' at the begining of property
*/
export const compareBy = (...props) => (a, b) => {
for (let i = 0; i < props.length; i++) {
const ascValue = props[i].startsWith('-') ? -1 : 1;
const prop = props[i].startsWith('-') ? props[i].substr(1) : props[i];
if (a[prop] !== b[prop]) {
return a[prop] > b[prop] ? ascValue : -ascValue;
}
}
return 0;
};
Example of usage (in your case):
homes.sort(compareBy('city', '-price'));
It should be noted that this function can be even more generalized in order to be able to use nested properties like 'address.city' or 'style.size.width' etc.

Another way
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sortBy(ar) {
return ar.sort((a, b) => a.city === b.city ?
b.price.toString().localeCompare(a.price) :
a.city.toString().localeCompare(b.city));
}
console.log(sortBy(homes));

simply follow the list of your sorting criteria
this code will always remain readable and understandable even if you have 36 sorting criteria to encase
The solution proposed here by Nina is certainly very elegant, but it implies knowing that a value of zero corresponds to a value of false in Boolean logic, and that Boolean tests can return something other than true / false in JavaScript (here are numeric values) which will always be confusing for a beginner.
Also think about who will need to maintain your code. Maybe it would be you: imagine yourself spending your days raking for days the code of another and having a pernicious bug ... and you are exhausted from reading these thousands of lines full of tips
const homes =
[ { h_id: '3', city: 'Dallas', state: 'TX', zip: '75201', price: '162500' }
, { h_id: '4', city: 'Bevery Hills', state: 'CA', zip: '90210', price: '319250' }
, { h_id: '6', city: 'Dallas', state: 'TX', zip: '75000', price: '556699' }
, { h_id: '5', city: 'New York', state: 'NY', zip: '00010', price: '962500' }
]
const fSort = (a,b) =>
{
let Dx = a.city.localeCompare(b.city) // 1st criteria
if (Dx===0) Dx = Number(b.price) - Number(a.price) // 2nd
// if (Dx===0) Dx = ... // 3rd
// if (Dx===0) Dx = ... // 4th....
return Dx
}
console.log( homes.sort(fSort))

Adding a couple helper functions lets you solved this kind of problem generically and simply. sortByKey takes an array and a function which should return a list of items with which to compare each array entry.
This takes advantage of the fact that javascript does smart comparison of arrays of simple values, with [2] < [2, 0] < [2, 1] < [10, 0].
// Two helpers:
function cmp(a, b) {
if (a > b) {
return 1
} else if (a < b) {
return -1
} else {
return 0
}
}
function sortByKey(arr, key) {
arr.sort((a, b) => cmp(key(a), key(b)))
}
// A demonstration:
let arr = [{a:1, b:2}, {b:3, a:0}, {a:1, b:1}, {a:2, b:2}, {a:2, b:1}, {a:1, b:10}]
sortByKey(arr, item => [item.a, item.b])
console.log(JSON.stringify(arr))
// '[{"b":3,"a":0},{"a":1,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":1},{"a":2,"b":2}]'
sortByKey(arr, item => [item.b, item.a])
console.log(JSON.stringify(arr))
// '[{"a":1,"b":1},{"a":2,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":2},{"b":3,"a":0}]'
I've lovingly stolen this idea from Python's list.sort function.

To make things simple, use these helper functions.
You can sort by as many fields as you need. For each sort field, specify the property name, and then, optionally, specify -1 as the sort direction to sort descending instead of ascending.
const data = [
{"h_id":"3","city":"Dallas","state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills","state":"CA","zip":"90210","price":"319250"},
{"h_id":"6","city":"Dallas","state":"TX","zip":"75000","price":"556699"},
{"h_id":"5","city":"New York","state":"NY","zip":"00010","price":"962500"},
{"h_id":"7","city":"New York","state":"NY","zip":"00010","price":"800500"}
]
const sortLexically = (p,d=1)=>(a,b)=>d * a[p].localeCompare(b[p])
const sortNumerically = (p,d=1)=>(a,b)=>d * (a[p]-b[p])
const sortBy = sorts=>(a,b)=>sorts.reduce((r,s)=>r||s(a,b),0)
// sort first by city, then by price descending
data.sort(sortBy([sortLexically('city'), sortNumerically('price', -1)]))
console.log(data)

Here is a generic version of #Snowburnt's solution:
var sortarray = [{field:'city', direction:'asc'}, {field:'price', direction:'desc'}];
array.sort(function(a,b){
for(var i=0; i<sortarray.length; i++){
retval = a[sortarray[i].field] < b[sortarray[i].field] ? -1 : a[sortarray[i].field] > b[sortarray[i].field] ? 1 : 0;
if (sortarray[i].direction == "desc") {
retval = retval * -1;
}
if (retval !== 0) {
return retval;
}
}
}
})
This is based on a sort routine I'm using. I didn't test this specific code so it may have errors but you get the idea. The idea is to sort based on the first field that indicates a difference and then stop and go to the next record. So, if you're sorting by three fields and the first field in the compare is enough to determine the sort order of the two records being sorted then return that sort result and go to the next record.
I tested it (actually with a little more complex sort logic) on 5000 records and it did it in the blink of an eye. If you're actually loading more than 1000 records to the client you should probably be using sever-side sorting and filtering.
This code isn't handling case-sensitivity but I leave it to the reader to handle this trivial modification.

function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
Using :
sort(homes, [{city : 'asc'}, {price: 'desc'}])
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
console.log(sort(homes, [{city : 'asc'}, {price: 'desc'}]));

// custom sorting by city
const sortArray = ['Dallas', 'New York', 'Beverly Hills'];
const sortData = (sortBy) =>
data
.sort((a, b) => {
const aIndex = sortBy.indexOf(a.city);
const bIndex = sortBy.indexOf(b.city);
if (aIndex < bIndex) {
return -1;
}
if (aIndex === bIndex) {
// price descending
return b.price- a.price;
}
return 1;
});
sortData(sortArray);

You can use lodash orderBy function lodash
It takes two params array of fields, and array of directions ('asc','desc')
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
var sorted =. data._.orderBy(data, ['city', 'price'], ['asc','desc'])

A very intuitive functional solution can be crafted by adding 3 relatively simple helpers. Before we dive in, let's start with the usage:
function usage(homes, { asc, desc, fallback }) {
homes.sort(fallback(
asc(home => home.city),
desc(home => parseInt(home.price, 10)),
));
console.log(homes);
}
var homes = [{
h_id: "3",
city: "Dallas",
state: "TX",
zip: "75201",
price: "162500",
}, {
h_id: "4",
city: "Bevery Hills",
state: "CA",
zip: "90210",
price: "319250",
}, {
h_id: "6",
city: "Dallas",
state: "TX",
zip: "75000",
price: "556699",
}, {
h_id: "5",
city: "New York",
state: "NY",
zip: "00010",
price: "962500",
}];
const SortHelpers = (function () {
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
return { asc, desc, fallback };
})();
usage(homes, SortHelpers);
If you scrolled down the snippet you probably already saw the helpers:
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
Let me quickly explain what each of these functions does.
asc creates a comparator function. The provided function fn is called for both the comparator arguments a and b. The results of the two function calls are then compared. -1 is returned if resultA < resultB, 1 is returned if resultA > resultB, or 0 otherwise. These return values correspond with an ascending order direction.
It could also be written like this:
function asc(fn) {
return function (a, b) {
// apply `fn` to both `a` and `b`
a = fn(a);
b = fn(b);
if (a < b) return -1;
if (a > b) return 1;
return 0;
// or `return -(a < b) || +(a > b)` for short
};
}
desc is super simple, since it just calls asc but swaps the a and b arguments, resulting in descending order instead of ascending.
fallback (there might be a better name for this) allows us to use multiple comparator functions with a single sort.
Both asc and desc can be passed to sort by themself.
homes.sort(asc(home => home.city))
There is however an issue if you want to combine multiple comparator functions. sort only accepts a single comparator function. fallback combines multiple comparator functions into a single comparator.
The first comparator is called with arguments a and b, if the comparator returns the value 0 (meaning that the values are equal) then we fall back to the next comparator. This continues until a non-0 value is found, or until all comparators are called, in which case the return value is 0.
You can provide your custom comparator functions to fallback() as well. Say you want to use localeCompare() instead of comparing strings with < and >. In such a case you can replace asc(home => home.city) with (a, b) => a.city.localeCompare(b.city).
homes.sort(fallback(
(a, b) => a.city.localeCompare(b.city),
desc(home => parseInt(home.price, 10)),
));
One thing to note is that values that can be undefined will always return false when comparing with < and >. So if a value can be missing you might want to sort by its presence first.
homes.sort(fallback(
// homes with optionalProperty first, true (1) > false (0) so we use desc
desc(home => home.optionalProperty != null), // checks for both null and undefined
asc(home => home.optionalProperty),
// ...
))
Since comparing strings with localeCompare() is such a common thing to do, you could include this as part of asc().
function hasMethod(item, methodName) {
return item != null && typeof item[methodName] === "function";
}
function asc(fn) {
return function (a, b) {
a = fn(a);
b = fn(b);
const areLocaleComparable =
hasMethod(a, "localeCompare") && hasMethod(b, "localeCompare");
if (areLocaleComparable) return a.localeCompare(b);
return -(a < b) || +(a > b);
};
}

Here, you can try the smaller and convenient way to sort by multiple fields!
var homes = [
{ "h_id": "3", "city": "Dallas", "state": "TX", "zip": "75201", "price": "162500" },
{ "h_id": "4", "city": "Bevery Hills", "state": "CA", "zip": "90210", "price": "319250" },
{ "h_id": "6", "city": "Dallas", "state": "TX", "zip": "75000", "price": "556699" },
{ "h_id": "5", "city": "New York", "state": "NY", "zip": "00010", "price": "962500" }
];
homes.sort((a, b)=> {
if (a.city === b.city){
return a.price < b.price ? -1 : 1
} else {
return a.city < b.city ? -1 : 1
}
})
console.log(homes);

function sortMultiFields(prop){
return function(a,b){
for(i=0;i<prop.length;i++)
{
var reg = /^\d+$/;
var x=1;
var field1=prop[i];
if(prop[i].indexOf("-")==0)
{
field1=prop[i].substr(1,prop[i].length);
x=-x;
}
if(reg.test(a[field1]))
{
a[field1]=parseFloat(a[field1]);
b[field1]=parseFloat(b[field1]);
}
if( a[field1] > b[field1])
return x;
else if(a[field1] < b[field1])
return -x;
}
}
}
How to use (put -(minus) sign before field if you want to sort in descending order particular field)
homes.sort(sortMultiFields(["city","-price"]));
Using above function you can sort any json array with multiple fields. No need to change function body at all

Adaptation of #chriskelly 's answer.
Most answers overlook that price will not sort properly if the value is in the ten thousands and lower or over a million. The resaon being JS sorts alphabetically. It was answered pretty well here, Why can't JavaScript sort "5, 10, 1" and here How to sort an array of integers correctly.
Ultimately we have to do some evaluation if the field or node we're sorting by is an number. I am not saying that using parseInt() in this case is the correct answer, the sorted results are more important.
var homes = [{
"h_id": "2",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62500"
}, {
"h_id": "1",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62510"
}, {
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "162500"
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": "319250"
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "75000",
"price": "556699"
}, {
"h_id": "5",
"city": "New York",
"state": "NY",
"zip": "00010",
"price": "962500"
}];
homes.sort(fieldSorter(['price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function(a, b) {
return fields
.map(function(o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o = o.substring(1);
}
if (!parseInt(a[o]) && !parseInt(b[o])) {
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
} else {
return dir > 0 ? a[o] - b[o] : b[o] - a[o];
}
})
.reduce(function firstNonZeroValue(p, n) {
return p ? p : n;
}, 0);
};
}
document.getElementById("output").innerHTML = '<pre>' + JSON.stringify(homes, null, '\t') + '</pre>';
<div id="output">
</div>
A fiddle to test with

Wow, there are some complex solutions here. So complex I decided to come up with something simpler but also quite powerful. Here it is;
function sortByPriority(data, priorities) {
if (priorities.length == 0) {
return data;
}
const nextPriority = priorities[0];
const remainingPriorities = priorities.slice(1);
const matched = data.filter(item => item.hasOwnProperty(nextPriority));
const remainingData = data.filter(item => !item.hasOwnProperty(nextPriority));
return sortByPriority(matched, remainingPriorities)
.sort((a, b) => (a[nextPriority] > b[nextPriority]) ? 1 : -1)
.concat(sortByPriority(remainingData, remainingPriorities));
}
And here is an example of how you use it.
const data = [
{ id: 1, mediumPriority: 'bbb', lowestPriority: 'ggg' },
{ id: 2, highestPriority: 'bbb', mediumPriority: 'ccc', lowestPriority: 'ggg' },
{ id: 3, mediumPriority: 'aaa', lowestPriority: 'ggg' },
];
const priorities = [
'highestPriority',
'mediumPriority',
'lowestPriority'
];
const sorted = sortByPriority(data, priorities);
This will first sort by the precedence of the attributes, then by the value of the attributes.

I think this may be the easiest way to do it.
https://coderwall.com/p/ebqhca/javascript-sort-by-two-fields
It's really simple and I tried it with 3 different key value pairs and it worked great.
Here is a simple example, look at the link for more details
testSort(data) {
return data.sort(
a['nameOne'] > b['nameOne'] ? 1
: b['nameOne'] > a['nameOne'] ? -1 : 0 ||
a['date'] > b['date'] ||
a['number'] - b['number']
);
}

Related

extend javascript sort with 3rd arguement [duplicate]

From this original question, how would I apply a sort on multiple fields?
Using this slightly adapted structure, how would I sort city (ascending) & then price (descending)?
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
I liked the fact than an answer was given which provided a general approach. Where I plan to use this code, I will have to sort dates as well as other things. The ability to "prime" the object seemed handy, if not a little cumbersome.
I've tried to build this answer into a nice generic example, but I'm not having much luck.
You could use a chained sorting approach by taking the delta of values until it reaches a value not equal to zero.
var data = [{ h_id: "3", city: "Dallas", state: "TX", zip: "75201", price: "162500" }, { h_id: "4", city: "Bevery Hills", state: "CA", zip: "90210", price: "319250" }, { h_id: "6", city: "Dallas", state: "TX", zip: "75000", price: "556699" }, { h_id: "5", city: "New York", state: "NY", zip: "00010", price: "962500" }];
data.sort(function (a, b) {
return a.city.localeCompare(b.city) || b.price - a.price;
});
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Or, using es6, simply:
data.sort((a, b) => a.city.localeCompare(b.city) || b.price - a.price);
for a non-generic, simple solution to your exact problem:
homes.sort(
function(a, b) {
if (a.city === b.city) {
// Price is only important when cities are the same
return b.price - a.price;
}
return a.city > b.city ? 1 : -1;
});
A multi dimensional sorting method, based on this answer:
Update: Here is an "optimized" version. It does a lot more preprocessing and creates a comparison function for each sorting option beforehand. It might need more more memory (as it stores a function for each sorting option, but it should preform a bit better as it does not have to determine the correct settings during the comparison. I have not done any profiling though.
var sort_by;
(function() {
// utility functions
var default_cmp = function(a, b) {
if (a == b) return 0;
return a < b ? -1 : 1;
},
getCmpFunc = function(primer, reverse) {
var dfc = default_cmp, // closer in scope
cmp = default_cmp;
if (primer) {
cmp = function(a, b) {
return dfc(primer(a), primer(b));
};
}
if (reverse) {
return function(a, b) {
return -1 * cmp(a, b);
};
}
return cmp;
};
// actual implementation
sort_by = function() {
var fields = [],
n_fields = arguments.length,
field, name, reverse, cmp;
// preprocess sorting options
for (var i = 0; i < n_fields; i++) {
field = arguments[i];
if (typeof field === 'string') {
name = field;
cmp = default_cmp;
}
else {
name = field.name;
cmp = getCmpFunc(field.primer, field.reverse);
}
fields.push({
name: name,
cmp: cmp
});
}
// final comparison function
return function(A, B) {
var a, b, name, result;
for (var i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
name = field.name;
result = field.cmp(A[name], B[name]);
if (result !== 0) break;
}
return result;
}
}
}());
Example usage:
homes.sort(sort_by('city', {name:'price', primer: parseInt, reverse: true}));
DEMO
Original function:
var sort_by = function() {
var fields = [].slice.call(arguments),
n_fields = fields.length;
return function(A,B) {
var a, b, field, key, primer, reverse, result, i;
for(i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
key = typeof field === 'string' ? field : field.name;
a = A[key];
b = B[key];
if (typeof field.primer !== 'undefined'){
a = field.primer(a);
b = field.primer(b);
}
reverse = (field.reverse) ? -1 : 1;
if (a<b) result = reverse * -1;
if (a>b) result = reverse * 1;
if(result !== 0) break;
}
return result;
}
};
DEMO
Here is a simple functional generic approach. Specify sort order using array. Prepend minus to specify descending order.
var homes = [
{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":"319250"},
{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":"556699"},
{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":"962500"}
];
homes.sort(fieldSorter(['city', '-price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function (a, b) {
return fields
.map(function (o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o=o.substring(1);
}
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
})
.reduce(function firstNonZeroValue (p,n) {
return p ? p : n;
}, 0);
};
}
Edit: in ES6 it's even shorter!
"use strict";
const fieldSorter = (fields) => (a, b) => fields.map(o => {
let dir = 1;
if (o[0] === '-') { dir = -1; o=o.substring(1); }
return a[o] > b[o] ? dir : a[o] < b[o] ? -(dir) : 0;
}).reduce((p, n) => p ? p : n, 0);
const homes = [{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":162500}, {"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":319250},{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":556699},{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":962500}];
const sortedHomes = homes.sort(fieldSorter(['state', '-price']));
document.write('<pre>' + JSON.stringify(sortedHomes, null, '\t') + '</pre>')
I made a quite generic multi feature sorter today. You can have a look at thenBy.js here: https://github.com/Teun/thenBy.js
It allows you to use the standard Array.sort, but with firstBy().thenBy().thenBy() style. It is way less code and complexity than the solutions posted above.
Here's an extensible way to sort by multiple fields.
homes.sort(function(left, right) {
var city_order = left.city.localeCompare(right.city);
var price_order = parseInt(left.price) - parseInt(right.price);
return city_order || -price_order;
});
Notes
A function passed to array sort is expected to return negative, zero, positive to indicate less, equal, greater.
a.localeCompare(b) is universally supported for strings, and returns -1,0,1 if a<b,a==b,a>b.
Subtraction works on numeric fields, because a - b gives -,0,+ if a<b,a==b,a>b.
|| in the last line gives city priority over price.
Negate to reverse order in any field, as in -price_order
Add new fields to the or-chain: return city_order || -price_order || date_order;
Date compare with subtraction, because date math converts to milliseconds since 1970.var date_order = new Date(left.date) - new Date(right.date);
Boolean compare with subtraction, which is guaranteed to turn true and false to 1 and 0 (therefore the subtraction produces -1 or 0 or 1). var goodness_order = Boolean(left.is_good) - Boolean(right.is_good)This is unusual enough that I'd suggest drawing attention with the Boolean constructor, even if they're already boolean.
This is a complete cheat but I think that it adds value to this question because it's basically a canned library function that you can use out-of-the box.
If your code has access to lodash or a lodash compatible library like underscore then you can use the _.sortBy method. The snippet below is copied directly from the lodash documentation.
The commented results in the examples looks like they return arrays of arrays but that's just showing the order and not the actual results which are an array of objects.
var users = [
{ 'user': 'fred',   'age': 48 },
{ 'user': 'barney', 'age': 36 },
{ 'user': 'fred',   'age': 40 },
{ 'user': 'barney', 'age': 34 }
];
_.sortBy(users, [function(o) { return o.user; }]);
// => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]
_.sortBy(users, ['user', 'age']);
// => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]]
The following function will allow you to sort an array of objects on one or multiple properties, either ascending (default) or descending on each property, and allow you to choose whether or not to perform case sensitive comparisons. By default, this function performs case insensitive sorts.
The first argument must be the array containing the objects.
The subsequent argument(s) must be a comma separated list of strings that reference the different object properties to sort by. The last argument (which is optional) is a boolean to choose whether or not to perform case sensitive sorts - use true for case sensitive sorts.
The function will sort each property/key in ascending order by default. If you want a particular key to sort in descending order, then instead pass in an array in this format: ['property_name', true].
Here are some sample uses of the function followed by an explanation (where homes is an array containing the objects):
objSort(homes, 'city') --> sort by city (ascending, case in-sensitive)
objSort(homes, ['city', true]) --> sort by city (descending, case in-sensitive)
objSort(homes, 'city', true) --> sort by city then price (ascending, case sensitive)
objSort(homes, 'city', 'price') --> sort by city then price (both ascending, case in-sensitive)
objSort(homes, 'city', ['price', true]) --> sort by city (ascending) then price (descending), case in-sensitive)
And without further ado, here's the function:
function objSort() {
var args = arguments,
array = args[0],
case_sensitive, keys_length, key, desc, a, b, i;
if (typeof arguments[arguments.length - 1] === 'boolean') {
case_sensitive = arguments[arguments.length - 1];
keys_length = arguments.length - 1;
} else {
case_sensitive = false;
keys_length = arguments.length;
}
return array.sort(function (obj1, obj2) {
for (i = 1; i < keys_length; i++) {
key = args[i];
if (typeof key !== 'string') {
desc = key[1];
key = key[0];
a = obj1[args[i][0]];
b = obj2[args[i][0]];
} else {
desc = false;
a = obj1[args[i]];
b = obj2[args[i]];
}
if (case_sensitive === false && typeof a === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (! desc) {
if (a < b) return -1;
if (a > b) return 1;
} else {
if (a > b) return -1;
if (a < b) return 1;
}
}
return 0;
});
} //end of objSort() function
And here's some sample data:
var homes = [{
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": 162500
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": 1000000
}, {
"h_id": "5",
"city": "new york",
"state": "NY",
"zip": "00010",
"price": 1000000
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "85000",
"price": 300000
}, {
"h_id": "7",
"city": "New York",
"state": "NY",
"zip": "00020",
"price": 345000
}];
A dynamic way to do that with MULTIPLE keys:
filter unique values from each col/key of sort
put in order or reverse it
add weights width zeropad for each object based on indexOf(value) keys values
sort using caclutated weights
Object.defineProperty(Array.prototype, 'orderBy', {
value: function(sorts) {
sorts.map(sort => {
sort.uniques = Array.from(
new Set(this.map(obj => obj[sort.key]))
);
sort.uniques = sort.uniques.sort((a, b) => {
if (typeof a == 'string') {
return sort.inverse ? b.localeCompare(a) : a.localeCompare(b);
}
else if (typeof a == 'number') {
return sort.inverse ? b - a : a - b;
}
else if (typeof a == 'boolean') {
let x = sort.inverse ? (a === b) ? 0 : a? -1 : 1 : (a === b) ? 0 : a? 1 : -1;
return x;
}
return 0;
});
});
const weightOfObject = (obj) => {
let weight = "";
sorts.map(sort => {
let zeropad = `${sort.uniques.length}`.length;
weight += sort.uniques.indexOf(obj[sort.key]).toString().padStart(zeropad, '0');
});
//obj.weight = weight; // if you need to see weights
return weight;
}
this.sort((a, b) => {
return weightOfObject(a).localeCompare( weightOfObject(b) );
});
return this;
}
});
Use:
// works with string, number and boolean
let sortered = your_array.orderBy([
{key: "type", inverse: false},
{key: "title", inverse: false},
{key: "spot", inverse: false},
{key: "internal", inverse: true}
]);
Here's a generic multidimensional sort, allowing for reversing and/or mapping on each level.
Written in Typescript. For Javascript, check out this JSFiddle
The Code
type itemMap = (n: any) => any;
interface SortConfig<T> {
key: keyof T;
reverse?: boolean;
map?: itemMap;
}
export function byObjectValues<T extends object>(keys: ((keyof T) | SortConfig<T>)[]): (a: T, b: T) => 0 | 1 | -1 {
return function(a: T, b: T) {
const firstKey: keyof T | SortConfig<T> = keys[0];
const isSimple = typeof firstKey === 'string';
const key: keyof T = isSimple ? (firstKey as keyof T) : (firstKey as SortConfig<T>).key;
const reverse: boolean = isSimple ? false : !!(firstKey as SortConfig<T>).reverse;
const map: itemMap | null = isSimple ? null : (firstKey as SortConfig<T>).map || null;
const valA = map ? map(a[key]) : a[key];
const valB = map ? map(b[key]) : b[key];
if (valA === valB) {
if (keys.length === 1) {
return 0;
}
return byObjectValues<T>(keys.slice(1))(a, b);
}
if (reverse) {
return valA > valB ? -1 : 1;
}
return valA > valB ? 1 : -1;
};
}
Usage Examples
Sorting a people array by last name, then first name:
interface Person {
firstName: string;
lastName: string;
}
people.sort(byObjectValues<Person>(['lastName','firstName']));
Sort language codes by their name, not their language code (see map), then by descending version (see reverse).
interface Language {
code: string;
version: number;
}
// languageCodeToName(code) is defined elsewhere in code
languageCodes.sort(byObjectValues<Language>([
{
key: 'code',
map(code:string) => languageCodeToName(code),
},
{
key: 'version',
reverse: true,
}
]));
Here's another one that's perhaps closer to your idea for the syntax
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {}; // primers are optional
properties = properties.map(function(prop) {
if( !(prop instanceof Array) ) {
prop = [prop, 'asc']
}
if( prop[1].toLowerCase() == 'desc' ) {
prop[1] = -1;
} else {
prop[1] = 1;
}
return prop;
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}
// just for fun use this to reverse the city name when sorting
function demoPrimer(str) {
return str.split('').reverse().join('');
}
// Example
sortObjects(homes, ['city', ['price', 'desc']], {city: demoPrimer});
Demo: http://jsfiddle.net/Nq4dk/2/
Edit: Just for fun, here's a variation that just takes an sql-like string, so you can do sortObjects(homes, "city, price desc")
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {};
properties = properties.split(/\s*,\s*/).map(function(prop) {
prop = prop.match(/^([^\s]+)(\s*desc)?/i);
if( prop[2] && prop[2].toLowerCase() === 'desc' ) {
return [prop[1] , -1];
} else {
return [prop[1] , 1];
}
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}
Simpler one:
var someArray = [...];
function generateSortFn(props) {
return function (a, b) {
for (var i = 0; i < props.length; i++) {
var prop = props[i];
var name = prop.name;
var reverse = prop.reverse;
if (a[name] < b[name])
return reverse ? 1 : -1;
if (a[name] > b[name])
return reverse ? -1 : 1;
}
return 0;
};
};
someArray.sort(generateSortFn([{name: 'prop1', reverse: true}, {name: 'prop2'}]));
why complicate? just sort it twice! this works perfectly:
(just make sure to reverse the importance order from least to most):
jj.sort( (a, b) => (a.id >= b.id) ? 1 : -1 );
jj.sort( (a, b) => (a.status >= b.status) ? 1 : -1 );
I like SnowBurnt's approach but it needs a tweak to test for equivalence on city NOT a difference.
homes.sort(
function(a,b){
if (a.city==b.city){
return (b.price-a.price);
} else {
return (a.city-b.city);
}
});
Here's my solution based on the Schwartzian transform idiom, hope you find it useful.
function sortByAttribute(array, ...attrs) {
// generate an array of predicate-objects contains
// property getter, and descending indicator
let predicates = attrs.map(pred => {
let descending = pred.charAt(0) === '-' ? -1 : 1;
pred = pred.replace(/^-/, '');
return {
getter: o => o[pred],
descend: descending
};
});
// schwartzian transform idiom implementation. aka: "decorate-sort-undecorate"
return array.map(item => {
return {
src: item,
compareValues: predicates.map(predicate => predicate.getter(item))
};
})
.sort((o1, o2) => {
let i = -1, result = 0;
while (++i < predicates.length) {
if (o1.compareValues[i] < o2.compareValues[i]) result = -1;
if (o1.compareValues[i] > o2.compareValues[i]) result = 1;
if (result *= predicates[i].descend) break;
}
return result;
})
.map(item => item.src);
}
Here's an example how to use it:
let games = [
{ name: 'Pako', rating: 4.21 },
{ name: 'Hill Climb Racing', rating: 3.88 },
{ name: 'Angry Birds Space', rating: 3.88 },
{ name: 'Badland', rating: 4.33 }
];
// sort by one attribute
console.log(sortByAttribute(games, 'name'));
// sort by mupltiple attributes
console.log(sortByAttribute(games, '-rating', 'name'));
Just another option. Consider to use the following utility function:
/** Performs comparing of two items by specified properties
* #param {Array} props for sorting ['name'], ['value', 'city'], ['-date']
* to set descending order on object property just add '-' at the begining of property
*/
export const compareBy = (...props) => (a, b) => {
for (let i = 0; i < props.length; i++) {
const ascValue = props[i].startsWith('-') ? -1 : 1;
const prop = props[i].startsWith('-') ? props[i].substr(1) : props[i];
if (a[prop] !== b[prop]) {
return a[prop] > b[prop] ? ascValue : -ascValue;
}
}
return 0;
};
Example of usage (in your case):
homes.sort(compareBy('city', '-price'));
It should be noted that this function can be even more generalized in order to be able to use nested properties like 'address.city' or 'style.size.width' etc.
Another way
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sortBy(ar) {
return ar.sort((a, b) => a.city === b.city ?
b.price.toString().localeCompare(a.price) :
a.city.toString().localeCompare(b.city));
}
console.log(sortBy(homes));
simply follow the list of your sorting criteria
this code will always remain readable and understandable even if you have 36 sorting criteria to encase
The solution proposed here by Nina is certainly very elegant, but it implies knowing that a value of zero corresponds to a value of false in Boolean logic, and that Boolean tests can return something other than true / false in JavaScript (here are numeric values) which will always be confusing for a beginner.
Also think about who will need to maintain your code. Maybe it would be you: imagine yourself spending your days raking for days the code of another and having a pernicious bug ... and you are exhausted from reading these thousands of lines full of tips
const homes =
[ { h_id: '3', city: 'Dallas', state: 'TX', zip: '75201', price: '162500' }
, { h_id: '4', city: 'Bevery Hills', state: 'CA', zip: '90210', price: '319250' }
, { h_id: '6', city: 'Dallas', state: 'TX', zip: '75000', price: '556699' }
, { h_id: '5', city: 'New York', state: 'NY', zip: '00010', price: '962500' }
]
const fSort = (a,b) =>
{
let Dx = a.city.localeCompare(b.city) // 1st criteria
if (Dx===0) Dx = Number(b.price) - Number(a.price) // 2nd
// if (Dx===0) Dx = ... // 3rd
// if (Dx===0) Dx = ... // 4th....
return Dx
}
console.log( homes.sort(fSort))
Adding a couple helper functions lets you solved this kind of problem generically and simply. sortByKey takes an array and a function which should return a list of items with which to compare each array entry.
This takes advantage of the fact that javascript does smart comparison of arrays of simple values, with [2] < [2, 0] < [2, 1] < [10, 0].
// Two helpers:
function cmp(a, b) {
if (a > b) {
return 1
} else if (a < b) {
return -1
} else {
return 0
}
}
function sortByKey(arr, key) {
arr.sort((a, b) => cmp(key(a), key(b)))
}
// A demonstration:
let arr = [{a:1, b:2}, {b:3, a:0}, {a:1, b:1}, {a:2, b:2}, {a:2, b:1}, {a:1, b:10}]
sortByKey(arr, item => [item.a, item.b])
console.log(JSON.stringify(arr))
// '[{"b":3,"a":0},{"a":1,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":1},{"a":2,"b":2}]'
sortByKey(arr, item => [item.b, item.a])
console.log(JSON.stringify(arr))
// '[{"a":1,"b":1},{"a":2,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":2},{"b":3,"a":0}]'
I've lovingly stolen this idea from Python's list.sort function.
To make things simple, use these helper functions.
You can sort by as many fields as you need. For each sort field, specify the property name, and then, optionally, specify -1 as the sort direction to sort descending instead of ascending.
const data = [
{"h_id":"3","city":"Dallas","state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills","state":"CA","zip":"90210","price":"319250"},
{"h_id":"6","city":"Dallas","state":"TX","zip":"75000","price":"556699"},
{"h_id":"5","city":"New York","state":"NY","zip":"00010","price":"962500"},
{"h_id":"7","city":"New York","state":"NY","zip":"00010","price":"800500"}
]
const sortLexically = (p,d=1)=>(a,b)=>d * a[p].localeCompare(b[p])
const sortNumerically = (p,d=1)=>(a,b)=>d * (a[p]-b[p])
const sortBy = sorts=>(a,b)=>sorts.reduce((r,s)=>r||s(a,b),0)
// sort first by city, then by price descending
data.sort(sortBy([sortLexically('city'), sortNumerically('price', -1)]))
console.log(data)
Here is a generic version of #Snowburnt's solution:
var sortarray = [{field:'city', direction:'asc'}, {field:'price', direction:'desc'}];
array.sort(function(a,b){
for(var i=0; i<sortarray.length; i++){
retval = a[sortarray[i].field] < b[sortarray[i].field] ? -1 : a[sortarray[i].field] > b[sortarray[i].field] ? 1 : 0;
if (sortarray[i].direction == "desc") {
retval = retval * -1;
}
if (retval !== 0) {
return retval;
}
}
}
})
This is based on a sort routine I'm using. I didn't test this specific code so it may have errors but you get the idea. The idea is to sort based on the first field that indicates a difference and then stop and go to the next record. So, if you're sorting by three fields and the first field in the compare is enough to determine the sort order of the two records being sorted then return that sort result and go to the next record.
I tested it (actually with a little more complex sort logic) on 5000 records and it did it in the blink of an eye. If you're actually loading more than 1000 records to the client you should probably be using sever-side sorting and filtering.
This code isn't handling case-sensitivity but I leave it to the reader to handle this trivial modification.
function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
Using :
sort(homes, [{city : 'asc'}, {price: 'desc'}])
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
console.log(sort(homes, [{city : 'asc'}, {price: 'desc'}]));
// custom sorting by city
const sortArray = ['Dallas', 'New York', 'Beverly Hills'];
const sortData = (sortBy) =>
data
.sort((a, b) => {
const aIndex = sortBy.indexOf(a.city);
const bIndex = sortBy.indexOf(b.city);
if (aIndex < bIndex) {
return -1;
}
if (aIndex === bIndex) {
// price descending
return b.price- a.price;
}
return 1;
});
sortData(sortArray);
You can use lodash orderBy function lodash
It takes two params array of fields, and array of directions ('asc','desc')
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
var sorted =. data._.orderBy(data, ['city', 'price'], ['asc','desc'])
A very intuitive functional solution can be crafted by adding 3 relatively simple helpers. Before we dive in, let's start with the usage:
function usage(homes, { asc, desc, fallback }) {
homes.sort(fallback(
asc(home => home.city),
desc(home => parseInt(home.price, 10)),
));
console.log(homes);
}
var homes = [{
h_id: "3",
city: "Dallas",
state: "TX",
zip: "75201",
price: "162500",
}, {
h_id: "4",
city: "Bevery Hills",
state: "CA",
zip: "90210",
price: "319250",
}, {
h_id: "6",
city: "Dallas",
state: "TX",
zip: "75000",
price: "556699",
}, {
h_id: "5",
city: "New York",
state: "NY",
zip: "00010",
price: "962500",
}];
const SortHelpers = (function () {
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
return { asc, desc, fallback };
})();
usage(homes, SortHelpers);
If you scrolled down the snippet you probably already saw the helpers:
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
Let me quickly explain what each of these functions does.
asc creates a comparator function. The provided function fn is called for both the comparator arguments a and b. The results of the two function calls are then compared. -1 is returned if resultA < resultB, 1 is returned if resultA > resultB, or 0 otherwise. These return values correspond with an ascending order direction.
It could also be written like this:
function asc(fn) {
return function (a, b) {
// apply `fn` to both `a` and `b`
a = fn(a);
b = fn(b);
if (a < b) return -1;
if (a > b) return 1;
return 0;
// or `return -(a < b) || +(a > b)` for short
};
}
desc is super simple, since it just calls asc but swaps the a and b arguments, resulting in descending order instead of ascending.
fallback (there might be a better name for this) allows us to use multiple comparator functions with a single sort.
Both asc and desc can be passed to sort by themself.
homes.sort(asc(home => home.city))
There is however an issue if you want to combine multiple comparator functions. sort only accepts a single comparator function. fallback combines multiple comparator functions into a single comparator.
The first comparator is called with arguments a and b, if the comparator returns the value 0 (meaning that the values are equal) then we fall back to the next comparator. This continues until a non-0 value is found, or until all comparators are called, in which case the return value is 0.
You can provide your custom comparator functions to fallback() as well. Say you want to use localeCompare() instead of comparing strings with < and >. In such a case you can replace asc(home => home.city) with (a, b) => a.city.localeCompare(b.city).
homes.sort(fallback(
(a, b) => a.city.localeCompare(b.city),
desc(home => parseInt(home.price, 10)),
));
One thing to note is that values that can be undefined will always return false when comparing with < and >. So if a value can be missing you might want to sort by its presence first.
homes.sort(fallback(
// homes with optionalProperty first, true (1) > false (0) so we use desc
desc(home => home.optionalProperty != null), // checks for both null and undefined
asc(home => home.optionalProperty),
// ...
))
Since comparing strings with localeCompare() is such a common thing to do, you could include this as part of asc().
function hasMethod(item, methodName) {
return item != null && typeof item[methodName] === "function";
}
function asc(fn) {
return function (a, b) {
a = fn(a);
b = fn(b);
const areLocaleComparable =
hasMethod(a, "localeCompare") && hasMethod(b, "localeCompare");
if (areLocaleComparable) return a.localeCompare(b);
return -(a < b) || +(a > b);
};
}
Here, you can try the smaller and convenient way to sort by multiple fields!
var homes = [
{ "h_id": "3", "city": "Dallas", "state": "TX", "zip": "75201", "price": "162500" },
{ "h_id": "4", "city": "Bevery Hills", "state": "CA", "zip": "90210", "price": "319250" },
{ "h_id": "6", "city": "Dallas", "state": "TX", "zip": "75000", "price": "556699" },
{ "h_id": "5", "city": "New York", "state": "NY", "zip": "00010", "price": "962500" }
];
homes.sort((a, b)=> {
if (a.city === b.city){
return a.price < b.price ? -1 : 1
} else {
return a.city < b.city ? -1 : 1
}
})
console.log(homes);
function sortMultiFields(prop){
return function(a,b){
for(i=0;i<prop.length;i++)
{
var reg = /^\d+$/;
var x=1;
var field1=prop[i];
if(prop[i].indexOf("-")==0)
{
field1=prop[i].substr(1,prop[i].length);
x=-x;
}
if(reg.test(a[field1]))
{
a[field1]=parseFloat(a[field1]);
b[field1]=parseFloat(b[field1]);
}
if( a[field1] > b[field1])
return x;
else if(a[field1] < b[field1])
return -x;
}
}
}
How to use (put -(minus) sign before field if you want to sort in descending order particular field)
homes.sort(sortMultiFields(["city","-price"]));
Using above function you can sort any json array with multiple fields. No need to change function body at all
Adaptation of #chriskelly 's answer.
Most answers overlook that price will not sort properly if the value is in the ten thousands and lower or over a million. The resaon being JS sorts alphabetically. It was answered pretty well here, Why can't JavaScript sort "5, 10, 1" and here How to sort an array of integers correctly.
Ultimately we have to do some evaluation if the field or node we're sorting by is an number. I am not saying that using parseInt() in this case is the correct answer, the sorted results are more important.
var homes = [{
"h_id": "2",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62500"
}, {
"h_id": "1",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62510"
}, {
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "162500"
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": "319250"
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "75000",
"price": "556699"
}, {
"h_id": "5",
"city": "New York",
"state": "NY",
"zip": "00010",
"price": "962500"
}];
homes.sort(fieldSorter(['price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function(a, b) {
return fields
.map(function(o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o = o.substring(1);
}
if (!parseInt(a[o]) && !parseInt(b[o])) {
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
} else {
return dir > 0 ? a[o] - b[o] : b[o] - a[o];
}
})
.reduce(function firstNonZeroValue(p, n) {
return p ? p : n;
}, 0);
};
}
document.getElementById("output").innerHTML = '<pre>' + JSON.stringify(homes, null, '\t') + '</pre>';
<div id="output">
</div>
A fiddle to test with
Wow, there are some complex solutions here. So complex I decided to come up with something simpler but also quite powerful. Here it is;
function sortByPriority(data, priorities) {
if (priorities.length == 0) {
return data;
}
const nextPriority = priorities[0];
const remainingPriorities = priorities.slice(1);
const matched = data.filter(item => item.hasOwnProperty(nextPriority));
const remainingData = data.filter(item => !item.hasOwnProperty(nextPriority));
return sortByPriority(matched, remainingPriorities)
.sort((a, b) => (a[nextPriority] > b[nextPriority]) ? 1 : -1)
.concat(sortByPriority(remainingData, remainingPriorities));
}
And here is an example of how you use it.
const data = [
{ id: 1, mediumPriority: 'bbb', lowestPriority: 'ggg' },
{ id: 2, highestPriority: 'bbb', mediumPriority: 'ccc', lowestPriority: 'ggg' },
{ id: 3, mediumPriority: 'aaa', lowestPriority: 'ggg' },
];
const priorities = [
'highestPriority',
'mediumPriority',
'lowestPriority'
];
const sorted = sortByPriority(data, priorities);
This will first sort by the precedence of the attributes, then by the value of the attributes.
I think this may be the easiest way to do it.
https://coderwall.com/p/ebqhca/javascript-sort-by-two-fields
It's really simple and I tried it with 3 different key value pairs and it worked great.
Here is a simple example, look at the link for more details
testSort(data) {
return data.sort(
a['nameOne'] > b['nameOne'] ? 1
: b['nameOne'] > a['nameOne'] ? -1 : 0 ||
a['date'] > b['date'] ||
a['number'] - b['number']
);
}

Sort object array by more than one comparison in one function [duplicate]

From this original question, how would I apply a sort on multiple fields?
Using this slightly adapted structure, how would I sort city (ascending) & then price (descending)?
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
I liked the fact than an answer was given which provided a general approach. Where I plan to use this code, I will have to sort dates as well as other things. The ability to "prime" the object seemed handy, if not a little cumbersome.
I've tried to build this answer into a nice generic example, but I'm not having much luck.
You could use a chained sorting approach by taking the delta of values until it reaches a value not equal to zero.
var data = [{ h_id: "3", city: "Dallas", state: "TX", zip: "75201", price: "162500" }, { h_id: "4", city: "Bevery Hills", state: "CA", zip: "90210", price: "319250" }, { h_id: "6", city: "Dallas", state: "TX", zip: "75000", price: "556699" }, { h_id: "5", city: "New York", state: "NY", zip: "00010", price: "962500" }];
data.sort(function (a, b) {
return a.city.localeCompare(b.city) || b.price - a.price;
});
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Or, using es6, simply:
data.sort((a, b) => a.city.localeCompare(b.city) || b.price - a.price);
for a non-generic, simple solution to your exact problem:
homes.sort(
function(a, b) {
if (a.city === b.city) {
// Price is only important when cities are the same
return b.price - a.price;
}
return a.city > b.city ? 1 : -1;
});
A multi dimensional sorting method, based on this answer:
Update: Here is an "optimized" version. It does a lot more preprocessing and creates a comparison function for each sorting option beforehand. It might need more more memory (as it stores a function for each sorting option, but it should preform a bit better as it does not have to determine the correct settings during the comparison. I have not done any profiling though.
var sort_by;
(function() {
// utility functions
var default_cmp = function(a, b) {
if (a == b) return 0;
return a < b ? -1 : 1;
},
getCmpFunc = function(primer, reverse) {
var dfc = default_cmp, // closer in scope
cmp = default_cmp;
if (primer) {
cmp = function(a, b) {
return dfc(primer(a), primer(b));
};
}
if (reverse) {
return function(a, b) {
return -1 * cmp(a, b);
};
}
return cmp;
};
// actual implementation
sort_by = function() {
var fields = [],
n_fields = arguments.length,
field, name, reverse, cmp;
// preprocess sorting options
for (var i = 0; i < n_fields; i++) {
field = arguments[i];
if (typeof field === 'string') {
name = field;
cmp = default_cmp;
}
else {
name = field.name;
cmp = getCmpFunc(field.primer, field.reverse);
}
fields.push({
name: name,
cmp: cmp
});
}
// final comparison function
return function(A, B) {
var a, b, name, result;
for (var i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
name = field.name;
result = field.cmp(A[name], B[name]);
if (result !== 0) break;
}
return result;
}
}
}());
Example usage:
homes.sort(sort_by('city', {name:'price', primer: parseInt, reverse: true}));
DEMO
Original function:
var sort_by = function() {
var fields = [].slice.call(arguments),
n_fields = fields.length;
return function(A,B) {
var a, b, field, key, primer, reverse, result, i;
for(i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
key = typeof field === 'string' ? field : field.name;
a = A[key];
b = B[key];
if (typeof field.primer !== 'undefined'){
a = field.primer(a);
b = field.primer(b);
}
reverse = (field.reverse) ? -1 : 1;
if (a<b) result = reverse * -1;
if (a>b) result = reverse * 1;
if(result !== 0) break;
}
return result;
}
};
DEMO
Here is a simple functional generic approach. Specify sort order using array. Prepend minus to specify descending order.
var homes = [
{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":"319250"},
{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":"556699"},
{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":"962500"}
];
homes.sort(fieldSorter(['city', '-price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function (a, b) {
return fields
.map(function (o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o=o.substring(1);
}
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
})
.reduce(function firstNonZeroValue (p,n) {
return p ? p : n;
}, 0);
};
}
Edit: in ES6 it's even shorter!
"use strict";
const fieldSorter = (fields) => (a, b) => fields.map(o => {
let dir = 1;
if (o[0] === '-') { dir = -1; o=o.substring(1); }
return a[o] > b[o] ? dir : a[o] < b[o] ? -(dir) : 0;
}).reduce((p, n) => p ? p : n, 0);
const homes = [{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":162500}, {"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":319250},{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":556699},{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":962500}];
const sortedHomes = homes.sort(fieldSorter(['state', '-price']));
document.write('<pre>' + JSON.stringify(sortedHomes, null, '\t') + '</pre>')
I made a quite generic multi feature sorter today. You can have a look at thenBy.js here: https://github.com/Teun/thenBy.js
It allows you to use the standard Array.sort, but with firstBy().thenBy().thenBy() style. It is way less code and complexity than the solutions posted above.
Here's an extensible way to sort by multiple fields.
homes.sort(function(left, right) {
var city_order = left.city.localeCompare(right.city);
var price_order = parseInt(left.price) - parseInt(right.price);
return city_order || -price_order;
});
Notes
A function passed to array sort is expected to return negative, zero, positive to indicate less, equal, greater.
a.localeCompare(b) is universally supported for strings, and returns -1,0,1 if a<b,a==b,a>b.
Subtraction works on numeric fields, because a - b gives -,0,+ if a<b,a==b,a>b.
|| in the last line gives city priority over price.
Negate to reverse order in any field, as in -price_order
Add new fields to the or-chain: return city_order || -price_order || date_order;
Date compare with subtraction, because date math converts to milliseconds since 1970.var date_order = new Date(left.date) - new Date(right.date);
Boolean compare with subtraction, which is guaranteed to turn true and false to 1 and 0 (therefore the subtraction produces -1 or 0 or 1). var goodness_order = Boolean(left.is_good) - Boolean(right.is_good)This is unusual enough that I'd suggest drawing attention with the Boolean constructor, even if they're already boolean.
This is a complete cheat but I think that it adds value to this question because it's basically a canned library function that you can use out-of-the box.
If your code has access to lodash or a lodash compatible library like underscore then you can use the _.sortBy method. The snippet below is copied directly from the lodash documentation.
The commented results in the examples looks like they return arrays of arrays but that's just showing the order and not the actual results which are an array of objects.
var users = [
{ 'user': 'fred',   'age': 48 },
{ 'user': 'barney', 'age': 36 },
{ 'user': 'fred',   'age': 40 },
{ 'user': 'barney', 'age': 34 }
];
_.sortBy(users, [function(o) { return o.user; }]);
// => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]
_.sortBy(users, ['user', 'age']);
// => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]]
The following function will allow you to sort an array of objects on one or multiple properties, either ascending (default) or descending on each property, and allow you to choose whether or not to perform case sensitive comparisons. By default, this function performs case insensitive sorts.
The first argument must be the array containing the objects.
The subsequent argument(s) must be a comma separated list of strings that reference the different object properties to sort by. The last argument (which is optional) is a boolean to choose whether or not to perform case sensitive sorts - use true for case sensitive sorts.
The function will sort each property/key in ascending order by default. If you want a particular key to sort in descending order, then instead pass in an array in this format: ['property_name', true].
Here are some sample uses of the function followed by an explanation (where homes is an array containing the objects):
objSort(homes, 'city') --> sort by city (ascending, case in-sensitive)
objSort(homes, ['city', true]) --> sort by city (descending, case in-sensitive)
objSort(homes, 'city', true) --> sort by city then price (ascending, case sensitive)
objSort(homes, 'city', 'price') --> sort by city then price (both ascending, case in-sensitive)
objSort(homes, 'city', ['price', true]) --> sort by city (ascending) then price (descending), case in-sensitive)
And without further ado, here's the function:
function objSort() {
var args = arguments,
array = args[0],
case_sensitive, keys_length, key, desc, a, b, i;
if (typeof arguments[arguments.length - 1] === 'boolean') {
case_sensitive = arguments[arguments.length - 1];
keys_length = arguments.length - 1;
} else {
case_sensitive = false;
keys_length = arguments.length;
}
return array.sort(function (obj1, obj2) {
for (i = 1; i < keys_length; i++) {
key = args[i];
if (typeof key !== 'string') {
desc = key[1];
key = key[0];
a = obj1[args[i][0]];
b = obj2[args[i][0]];
} else {
desc = false;
a = obj1[args[i]];
b = obj2[args[i]];
}
if (case_sensitive === false && typeof a === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (! desc) {
if (a < b) return -1;
if (a > b) return 1;
} else {
if (a > b) return -1;
if (a < b) return 1;
}
}
return 0;
});
} //end of objSort() function
And here's some sample data:
var homes = [{
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": 162500
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": 1000000
}, {
"h_id": "5",
"city": "new york",
"state": "NY",
"zip": "00010",
"price": 1000000
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "85000",
"price": 300000
}, {
"h_id": "7",
"city": "New York",
"state": "NY",
"zip": "00020",
"price": 345000
}];
A dynamic way to do that with MULTIPLE keys:
filter unique values from each col/key of sort
put in order or reverse it
add weights width zeropad for each object based on indexOf(value) keys values
sort using caclutated weights
Object.defineProperty(Array.prototype, 'orderBy', {
value: function(sorts) {
sorts.map(sort => {
sort.uniques = Array.from(
new Set(this.map(obj => obj[sort.key]))
);
sort.uniques = sort.uniques.sort((a, b) => {
if (typeof a == 'string') {
return sort.inverse ? b.localeCompare(a) : a.localeCompare(b);
}
else if (typeof a == 'number') {
return sort.inverse ? b - a : a - b;
}
else if (typeof a == 'boolean') {
let x = sort.inverse ? (a === b) ? 0 : a? -1 : 1 : (a === b) ? 0 : a? 1 : -1;
return x;
}
return 0;
});
});
const weightOfObject = (obj) => {
let weight = "";
sorts.map(sort => {
let zeropad = `${sort.uniques.length}`.length;
weight += sort.uniques.indexOf(obj[sort.key]).toString().padStart(zeropad, '0');
});
//obj.weight = weight; // if you need to see weights
return weight;
}
this.sort((a, b) => {
return weightOfObject(a).localeCompare( weightOfObject(b) );
});
return this;
}
});
Use:
// works with string, number and boolean
let sortered = your_array.orderBy([
{key: "type", inverse: false},
{key: "title", inverse: false},
{key: "spot", inverse: false},
{key: "internal", inverse: true}
]);
Here's a generic multidimensional sort, allowing for reversing and/or mapping on each level.
Written in Typescript. For Javascript, check out this JSFiddle
The Code
type itemMap = (n: any) => any;
interface SortConfig<T> {
key: keyof T;
reverse?: boolean;
map?: itemMap;
}
export function byObjectValues<T extends object>(keys: ((keyof T) | SortConfig<T>)[]): (a: T, b: T) => 0 | 1 | -1 {
return function(a: T, b: T) {
const firstKey: keyof T | SortConfig<T> = keys[0];
const isSimple = typeof firstKey === 'string';
const key: keyof T = isSimple ? (firstKey as keyof T) : (firstKey as SortConfig<T>).key;
const reverse: boolean = isSimple ? false : !!(firstKey as SortConfig<T>).reverse;
const map: itemMap | null = isSimple ? null : (firstKey as SortConfig<T>).map || null;
const valA = map ? map(a[key]) : a[key];
const valB = map ? map(b[key]) : b[key];
if (valA === valB) {
if (keys.length === 1) {
return 0;
}
return byObjectValues<T>(keys.slice(1))(a, b);
}
if (reverse) {
return valA > valB ? -1 : 1;
}
return valA > valB ? 1 : -1;
};
}
Usage Examples
Sorting a people array by last name, then first name:
interface Person {
firstName: string;
lastName: string;
}
people.sort(byObjectValues<Person>(['lastName','firstName']));
Sort language codes by their name, not their language code (see map), then by descending version (see reverse).
interface Language {
code: string;
version: number;
}
// languageCodeToName(code) is defined elsewhere in code
languageCodes.sort(byObjectValues<Language>([
{
key: 'code',
map(code:string) => languageCodeToName(code),
},
{
key: 'version',
reverse: true,
}
]));
Here's another one that's perhaps closer to your idea for the syntax
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {}; // primers are optional
properties = properties.map(function(prop) {
if( !(prop instanceof Array) ) {
prop = [prop, 'asc']
}
if( prop[1].toLowerCase() == 'desc' ) {
prop[1] = -1;
} else {
prop[1] = 1;
}
return prop;
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}
// just for fun use this to reverse the city name when sorting
function demoPrimer(str) {
return str.split('').reverse().join('');
}
// Example
sortObjects(homes, ['city', ['price', 'desc']], {city: demoPrimer});
Demo: http://jsfiddle.net/Nq4dk/2/
Edit: Just for fun, here's a variation that just takes an sql-like string, so you can do sortObjects(homes, "city, price desc")
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {};
properties = properties.split(/\s*,\s*/).map(function(prop) {
prop = prop.match(/^([^\s]+)(\s*desc)?/i);
if( prop[2] && prop[2].toLowerCase() === 'desc' ) {
return [prop[1] , -1];
} else {
return [prop[1] , 1];
}
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}
Simpler one:
var someArray = [...];
function generateSortFn(props) {
return function (a, b) {
for (var i = 0; i < props.length; i++) {
var prop = props[i];
var name = prop.name;
var reverse = prop.reverse;
if (a[name] < b[name])
return reverse ? 1 : -1;
if (a[name] > b[name])
return reverse ? -1 : 1;
}
return 0;
};
};
someArray.sort(generateSortFn([{name: 'prop1', reverse: true}, {name: 'prop2'}]));
why complicate? just sort it twice! this works perfectly:
(just make sure to reverse the importance order from least to most):
jj.sort( (a, b) => (a.id >= b.id) ? 1 : -1 );
jj.sort( (a, b) => (a.status >= b.status) ? 1 : -1 );
I like SnowBurnt's approach but it needs a tweak to test for equivalence on city NOT a difference.
homes.sort(
function(a,b){
if (a.city==b.city){
return (b.price-a.price);
} else {
return (a.city-b.city);
}
});
Here's my solution based on the Schwartzian transform idiom, hope you find it useful.
function sortByAttribute(array, ...attrs) {
// generate an array of predicate-objects contains
// property getter, and descending indicator
let predicates = attrs.map(pred => {
let descending = pred.charAt(0) === '-' ? -1 : 1;
pred = pred.replace(/^-/, '');
return {
getter: o => o[pred],
descend: descending
};
});
// schwartzian transform idiom implementation. aka: "decorate-sort-undecorate"
return array.map(item => {
return {
src: item,
compareValues: predicates.map(predicate => predicate.getter(item))
};
})
.sort((o1, o2) => {
let i = -1, result = 0;
while (++i < predicates.length) {
if (o1.compareValues[i] < o2.compareValues[i]) result = -1;
if (o1.compareValues[i] > o2.compareValues[i]) result = 1;
if (result *= predicates[i].descend) break;
}
return result;
})
.map(item => item.src);
}
Here's an example how to use it:
let games = [
{ name: 'Pako', rating: 4.21 },
{ name: 'Hill Climb Racing', rating: 3.88 },
{ name: 'Angry Birds Space', rating: 3.88 },
{ name: 'Badland', rating: 4.33 }
];
// sort by one attribute
console.log(sortByAttribute(games, 'name'));
// sort by mupltiple attributes
console.log(sortByAttribute(games, '-rating', 'name'));
Just another option. Consider to use the following utility function:
/** Performs comparing of two items by specified properties
* #param {Array} props for sorting ['name'], ['value', 'city'], ['-date']
* to set descending order on object property just add '-' at the begining of property
*/
export const compareBy = (...props) => (a, b) => {
for (let i = 0; i < props.length; i++) {
const ascValue = props[i].startsWith('-') ? -1 : 1;
const prop = props[i].startsWith('-') ? props[i].substr(1) : props[i];
if (a[prop] !== b[prop]) {
return a[prop] > b[prop] ? ascValue : -ascValue;
}
}
return 0;
};
Example of usage (in your case):
homes.sort(compareBy('city', '-price'));
It should be noted that this function can be even more generalized in order to be able to use nested properties like 'address.city' or 'style.size.width' etc.
Another way
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sortBy(ar) {
return ar.sort((a, b) => a.city === b.city ?
b.price.toString().localeCompare(a.price) :
a.city.toString().localeCompare(b.city));
}
console.log(sortBy(homes));
simply follow the list of your sorting criteria
this code will always remain readable and understandable even if you have 36 sorting criteria to encase
The solution proposed here by Nina is certainly very elegant, but it implies knowing that a value of zero corresponds to a value of false in Boolean logic, and that Boolean tests can return something other than true / false in JavaScript (here are numeric values) which will always be confusing for a beginner.
Also think about who will need to maintain your code. Maybe it would be you: imagine yourself spending your days raking for days the code of another and having a pernicious bug ... and you are exhausted from reading these thousands of lines full of tips
const homes =
[ { h_id: '3', city: 'Dallas', state: 'TX', zip: '75201', price: '162500' }
, { h_id: '4', city: 'Bevery Hills', state: 'CA', zip: '90210', price: '319250' }
, { h_id: '6', city: 'Dallas', state: 'TX', zip: '75000', price: '556699' }
, { h_id: '5', city: 'New York', state: 'NY', zip: '00010', price: '962500' }
]
const fSort = (a,b) =>
{
let Dx = a.city.localeCompare(b.city) // 1st criteria
if (Dx===0) Dx = Number(b.price) - Number(a.price) // 2nd
// if (Dx===0) Dx = ... // 3rd
// if (Dx===0) Dx = ... // 4th....
return Dx
}
console.log( homes.sort(fSort))
Adding a couple helper functions lets you solved this kind of problem generically and simply. sortByKey takes an array and a function which should return a list of items with which to compare each array entry.
This takes advantage of the fact that javascript does smart comparison of arrays of simple values, with [2] < [2, 0] < [2, 1] < [10, 0].
// Two helpers:
function cmp(a, b) {
if (a > b) {
return 1
} else if (a < b) {
return -1
} else {
return 0
}
}
function sortByKey(arr, key) {
arr.sort((a, b) => cmp(key(a), key(b)))
}
// A demonstration:
let arr = [{a:1, b:2}, {b:3, a:0}, {a:1, b:1}, {a:2, b:2}, {a:2, b:1}, {a:1, b:10}]
sortByKey(arr, item => [item.a, item.b])
console.log(JSON.stringify(arr))
// '[{"b":3,"a":0},{"a":1,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":1},{"a":2,"b":2}]'
sortByKey(arr, item => [item.b, item.a])
console.log(JSON.stringify(arr))
// '[{"a":1,"b":1},{"a":2,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":2},{"b":3,"a":0}]'
I've lovingly stolen this idea from Python's list.sort function.
To make things simple, use these helper functions.
You can sort by as many fields as you need. For each sort field, specify the property name, and then, optionally, specify -1 as the sort direction to sort descending instead of ascending.
const data = [
{"h_id":"3","city":"Dallas","state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills","state":"CA","zip":"90210","price":"319250"},
{"h_id":"6","city":"Dallas","state":"TX","zip":"75000","price":"556699"},
{"h_id":"5","city":"New York","state":"NY","zip":"00010","price":"962500"},
{"h_id":"7","city":"New York","state":"NY","zip":"00010","price":"800500"}
]
const sortLexically = (p,d=1)=>(a,b)=>d * a[p].localeCompare(b[p])
const sortNumerically = (p,d=1)=>(a,b)=>d * (a[p]-b[p])
const sortBy = sorts=>(a,b)=>sorts.reduce((r,s)=>r||s(a,b),0)
// sort first by city, then by price descending
data.sort(sortBy([sortLexically('city'), sortNumerically('price', -1)]))
console.log(data)
Here is a generic version of #Snowburnt's solution:
var sortarray = [{field:'city', direction:'asc'}, {field:'price', direction:'desc'}];
array.sort(function(a,b){
for(var i=0; i<sortarray.length; i++){
retval = a[sortarray[i].field] < b[sortarray[i].field] ? -1 : a[sortarray[i].field] > b[sortarray[i].field] ? 1 : 0;
if (sortarray[i].direction == "desc") {
retval = retval * -1;
}
if (retval !== 0) {
return retval;
}
}
}
})
This is based on a sort routine I'm using. I didn't test this specific code so it may have errors but you get the idea. The idea is to sort based on the first field that indicates a difference and then stop and go to the next record. So, if you're sorting by three fields and the first field in the compare is enough to determine the sort order of the two records being sorted then return that sort result and go to the next record.
I tested it (actually with a little more complex sort logic) on 5000 records and it did it in the blink of an eye. If you're actually loading more than 1000 records to the client you should probably be using sever-side sorting and filtering.
This code isn't handling case-sensitivity but I leave it to the reader to handle this trivial modification.
function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
Using :
sort(homes, [{city : 'asc'}, {price: 'desc'}])
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
console.log(sort(homes, [{city : 'asc'}, {price: 'desc'}]));
// custom sorting by city
const sortArray = ['Dallas', 'New York', 'Beverly Hills'];
const sortData = (sortBy) =>
data
.sort((a, b) => {
const aIndex = sortBy.indexOf(a.city);
const bIndex = sortBy.indexOf(b.city);
if (aIndex < bIndex) {
return -1;
}
if (aIndex === bIndex) {
// price descending
return b.price- a.price;
}
return 1;
});
sortData(sortArray);
You can use lodash orderBy function lodash
It takes two params array of fields, and array of directions ('asc','desc')
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
var sorted =. data._.orderBy(data, ['city', 'price'], ['asc','desc'])
A very intuitive functional solution can be crafted by adding 3 relatively simple helpers. Before we dive in, let's start with the usage:
function usage(homes, { asc, desc, fallback }) {
homes.sort(fallback(
asc(home => home.city),
desc(home => parseInt(home.price, 10)),
));
console.log(homes);
}
var homes = [{
h_id: "3",
city: "Dallas",
state: "TX",
zip: "75201",
price: "162500",
}, {
h_id: "4",
city: "Bevery Hills",
state: "CA",
zip: "90210",
price: "319250",
}, {
h_id: "6",
city: "Dallas",
state: "TX",
zip: "75000",
price: "556699",
}, {
h_id: "5",
city: "New York",
state: "NY",
zip: "00010",
price: "962500",
}];
const SortHelpers = (function () {
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
return { asc, desc, fallback };
})();
usage(homes, SortHelpers);
If you scrolled down the snippet you probably already saw the helpers:
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
Let me quickly explain what each of these functions does.
asc creates a comparator function. The provided function fn is called for both the comparator arguments a and b. The results of the two function calls are then compared. -1 is returned if resultA < resultB, 1 is returned if resultA > resultB, or 0 otherwise. These return values correspond with an ascending order direction.
It could also be written like this:
function asc(fn) {
return function (a, b) {
// apply `fn` to both `a` and `b`
a = fn(a);
b = fn(b);
if (a < b) return -1;
if (a > b) return 1;
return 0;
// or `return -(a < b) || +(a > b)` for short
};
}
desc is super simple, since it just calls asc but swaps the a and b arguments, resulting in descending order instead of ascending.
fallback (there might be a better name for this) allows us to use multiple comparator functions with a single sort.
Both asc and desc can be passed to sort by themself.
homes.sort(asc(home => home.city))
There is however an issue if you want to combine multiple comparator functions. sort only accepts a single comparator function. fallback combines multiple comparator functions into a single comparator.
The first comparator is called with arguments a and b, if the comparator returns the value 0 (meaning that the values are equal) then we fall back to the next comparator. This continues until a non-0 value is found, or until all comparators are called, in which case the return value is 0.
You can provide your custom comparator functions to fallback() as well. Say you want to use localeCompare() instead of comparing strings with < and >. In such a case you can replace asc(home => home.city) with (a, b) => a.city.localeCompare(b.city).
homes.sort(fallback(
(a, b) => a.city.localeCompare(b.city),
desc(home => parseInt(home.price, 10)),
));
One thing to note is that values that can be undefined will always return false when comparing with < and >. So if a value can be missing you might want to sort by its presence first.
homes.sort(fallback(
// homes with optionalProperty first, true (1) > false (0) so we use desc
desc(home => home.optionalProperty != null), // checks for both null and undefined
asc(home => home.optionalProperty),
// ...
))
Since comparing strings with localeCompare() is such a common thing to do, you could include this as part of asc().
function hasMethod(item, methodName) {
return item != null && typeof item[methodName] === "function";
}
function asc(fn) {
return function (a, b) {
a = fn(a);
b = fn(b);
const areLocaleComparable =
hasMethod(a, "localeCompare") && hasMethod(b, "localeCompare");
if (areLocaleComparable) return a.localeCompare(b);
return -(a < b) || +(a > b);
};
}
Here, you can try the smaller and convenient way to sort by multiple fields!
var homes = [
{ "h_id": "3", "city": "Dallas", "state": "TX", "zip": "75201", "price": "162500" },
{ "h_id": "4", "city": "Bevery Hills", "state": "CA", "zip": "90210", "price": "319250" },
{ "h_id": "6", "city": "Dallas", "state": "TX", "zip": "75000", "price": "556699" },
{ "h_id": "5", "city": "New York", "state": "NY", "zip": "00010", "price": "962500" }
];
homes.sort((a, b)=> {
if (a.city === b.city){
return a.price < b.price ? -1 : 1
} else {
return a.city < b.city ? -1 : 1
}
})
console.log(homes);
function sortMultiFields(prop){
return function(a,b){
for(i=0;i<prop.length;i++)
{
var reg = /^\d+$/;
var x=1;
var field1=prop[i];
if(prop[i].indexOf("-")==0)
{
field1=prop[i].substr(1,prop[i].length);
x=-x;
}
if(reg.test(a[field1]))
{
a[field1]=parseFloat(a[field1]);
b[field1]=parseFloat(b[field1]);
}
if( a[field1] > b[field1])
return x;
else if(a[field1] < b[field1])
return -x;
}
}
}
How to use (put -(minus) sign before field if you want to sort in descending order particular field)
homes.sort(sortMultiFields(["city","-price"]));
Using above function you can sort any json array with multiple fields. No need to change function body at all
Adaptation of #chriskelly 's answer.
Most answers overlook that price will not sort properly if the value is in the ten thousands and lower or over a million. The resaon being JS sorts alphabetically. It was answered pretty well here, Why can't JavaScript sort "5, 10, 1" and here How to sort an array of integers correctly.
Ultimately we have to do some evaluation if the field or node we're sorting by is an number. I am not saying that using parseInt() in this case is the correct answer, the sorted results are more important.
var homes = [{
"h_id": "2",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62500"
}, {
"h_id": "1",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62510"
}, {
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "162500"
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": "319250"
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "75000",
"price": "556699"
}, {
"h_id": "5",
"city": "New York",
"state": "NY",
"zip": "00010",
"price": "962500"
}];
homes.sort(fieldSorter(['price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function(a, b) {
return fields
.map(function(o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o = o.substring(1);
}
if (!parseInt(a[o]) && !parseInt(b[o])) {
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
} else {
return dir > 0 ? a[o] - b[o] : b[o] - a[o];
}
})
.reduce(function firstNonZeroValue(p, n) {
return p ? p : n;
}, 0);
};
}
document.getElementById("output").innerHTML = '<pre>' + JSON.stringify(homes, null, '\t') + '</pre>';
<div id="output">
</div>
A fiddle to test with
Wow, there are some complex solutions here. So complex I decided to come up with something simpler but also quite powerful. Here it is;
function sortByPriority(data, priorities) {
if (priorities.length == 0) {
return data;
}
const nextPriority = priorities[0];
const remainingPriorities = priorities.slice(1);
const matched = data.filter(item => item.hasOwnProperty(nextPriority));
const remainingData = data.filter(item => !item.hasOwnProperty(nextPriority));
return sortByPriority(matched, remainingPriorities)
.sort((a, b) => (a[nextPriority] > b[nextPriority]) ? 1 : -1)
.concat(sortByPriority(remainingData, remainingPriorities));
}
And here is an example of how you use it.
const data = [
{ id: 1, mediumPriority: 'bbb', lowestPriority: 'ggg' },
{ id: 2, highestPriority: 'bbb', mediumPriority: 'ccc', lowestPriority: 'ggg' },
{ id: 3, mediumPriority: 'aaa', lowestPriority: 'ggg' },
];
const priorities = [
'highestPriority',
'mediumPriority',
'lowestPriority'
];
const sorted = sortByPriority(data, priorities);
This will first sort by the precedence of the attributes, then by the value of the attributes.
I think this may be the easiest way to do it.
https://coderwall.com/p/ebqhca/javascript-sort-by-two-fields
It's really simple and I tried it with 3 different key value pairs and it worked great.
Here is a simple example, look at the link for more details
testSort(data) {
return data.sort(
a['nameOne'] > b['nameOne'] ? 1
: b['nameOne'] > a['nameOne'] ? -1 : 0 ||
a['date'] > b['date'] ||
a['number'] - b['number']
);
}

javascript – sort arrays at multiple levels for a bibliography [duplicate]

From this original question, how would I apply a sort on multiple fields?
Using this slightly adapted structure, how would I sort city (ascending) & then price (descending)?
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
I liked the fact than an answer was given which provided a general approach. Where I plan to use this code, I will have to sort dates as well as other things. The ability to "prime" the object seemed handy, if not a little cumbersome.
I've tried to build this answer into a nice generic example, but I'm not having much luck.
You could use a chained sorting approach by taking the delta of values until it reaches a value not equal to zero.
var data = [{ h_id: "3", city: "Dallas", state: "TX", zip: "75201", price: "162500" }, { h_id: "4", city: "Bevery Hills", state: "CA", zip: "90210", price: "319250" }, { h_id: "6", city: "Dallas", state: "TX", zip: "75000", price: "556699" }, { h_id: "5", city: "New York", state: "NY", zip: "00010", price: "962500" }];
data.sort(function (a, b) {
return a.city.localeCompare(b.city) || b.price - a.price;
});
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Or, using es6, simply:
data.sort((a, b) => a.city.localeCompare(b.city) || b.price - a.price);
for a non-generic, simple solution to your exact problem:
homes.sort(
function(a, b) {
if (a.city === b.city) {
// Price is only important when cities are the same
return b.price - a.price;
}
return a.city > b.city ? 1 : -1;
});
A multi dimensional sorting method, based on this answer:
Update: Here is an "optimized" version. It does a lot more preprocessing and creates a comparison function for each sorting option beforehand. It might need more more memory (as it stores a function for each sorting option, but it should preform a bit better as it does not have to determine the correct settings during the comparison. I have not done any profiling though.
var sort_by;
(function() {
// utility functions
var default_cmp = function(a, b) {
if (a == b) return 0;
return a < b ? -1 : 1;
},
getCmpFunc = function(primer, reverse) {
var dfc = default_cmp, // closer in scope
cmp = default_cmp;
if (primer) {
cmp = function(a, b) {
return dfc(primer(a), primer(b));
};
}
if (reverse) {
return function(a, b) {
return -1 * cmp(a, b);
};
}
return cmp;
};
// actual implementation
sort_by = function() {
var fields = [],
n_fields = arguments.length,
field, name, reverse, cmp;
// preprocess sorting options
for (var i = 0; i < n_fields; i++) {
field = arguments[i];
if (typeof field === 'string') {
name = field;
cmp = default_cmp;
}
else {
name = field.name;
cmp = getCmpFunc(field.primer, field.reverse);
}
fields.push({
name: name,
cmp: cmp
});
}
// final comparison function
return function(A, B) {
var a, b, name, result;
for (var i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
name = field.name;
result = field.cmp(A[name], B[name]);
if (result !== 0) break;
}
return result;
}
}
}());
Example usage:
homes.sort(sort_by('city', {name:'price', primer: parseInt, reverse: true}));
DEMO
Original function:
var sort_by = function() {
var fields = [].slice.call(arguments),
n_fields = fields.length;
return function(A,B) {
var a, b, field, key, primer, reverse, result, i;
for(i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
key = typeof field === 'string' ? field : field.name;
a = A[key];
b = B[key];
if (typeof field.primer !== 'undefined'){
a = field.primer(a);
b = field.primer(b);
}
reverse = (field.reverse) ? -1 : 1;
if (a<b) result = reverse * -1;
if (a>b) result = reverse * 1;
if(result !== 0) break;
}
return result;
}
};
DEMO
Here is a simple functional generic approach. Specify sort order using array. Prepend minus to specify descending order.
var homes = [
{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":"319250"},
{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":"556699"},
{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":"962500"}
];
homes.sort(fieldSorter(['city', '-price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function (a, b) {
return fields
.map(function (o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o=o.substring(1);
}
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
})
.reduce(function firstNonZeroValue (p,n) {
return p ? p : n;
}, 0);
};
}
Edit: in ES6 it's even shorter!
"use strict";
const fieldSorter = (fields) => (a, b) => fields.map(o => {
let dir = 1;
if (o[0] === '-') { dir = -1; o=o.substring(1); }
return a[o] > b[o] ? dir : a[o] < b[o] ? -(dir) : 0;
}).reduce((p, n) => p ? p : n, 0);
const homes = [{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":162500}, {"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":319250},{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":556699},{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":962500}];
const sortedHomes = homes.sort(fieldSorter(['state', '-price']));
document.write('<pre>' + JSON.stringify(sortedHomes, null, '\t') + '</pre>')
I made a quite generic multi feature sorter today. You can have a look at thenBy.js here: https://github.com/Teun/thenBy.js
It allows you to use the standard Array.sort, but with firstBy().thenBy().thenBy() style. It is way less code and complexity than the solutions posted above.
Here's an extensible way to sort by multiple fields.
homes.sort(function(left, right) {
var city_order = left.city.localeCompare(right.city);
var price_order = parseInt(left.price) - parseInt(right.price);
return city_order || -price_order;
});
Notes
A function passed to array sort is expected to return negative, zero, positive to indicate less, equal, greater.
a.localeCompare(b) is universally supported for strings, and returns -1,0,1 if a<b,a==b,a>b.
Subtraction works on numeric fields, because a - b gives -,0,+ if a<b,a==b,a>b.
|| in the last line gives city priority over price.
Negate to reverse order in any field, as in -price_order
Add new fields to the or-chain: return city_order || -price_order || date_order;
Date compare with subtraction, because date math converts to milliseconds since 1970.var date_order = new Date(left.date) - new Date(right.date);
Boolean compare with subtraction, which is guaranteed to turn true and false to 1 and 0 (therefore the subtraction produces -1 or 0 or 1). var goodness_order = Boolean(left.is_good) - Boolean(right.is_good)This is unusual enough that I'd suggest drawing attention with the Boolean constructor, even if they're already boolean.
This is a complete cheat but I think that it adds value to this question because it's basically a canned library function that you can use out-of-the box.
If your code has access to lodash or a lodash compatible library like underscore then you can use the _.sortBy method. The snippet below is copied directly from the lodash documentation.
The commented results in the examples looks like they return arrays of arrays but that's just showing the order and not the actual results which are an array of objects.
var users = [
{ 'user': 'fred',   'age': 48 },
{ 'user': 'barney', 'age': 36 },
{ 'user': 'fred',   'age': 40 },
{ 'user': 'barney', 'age': 34 }
];
_.sortBy(users, [function(o) { return o.user; }]);
// => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]
_.sortBy(users, ['user', 'age']);
// => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]]
The following function will allow you to sort an array of objects on one or multiple properties, either ascending (default) or descending on each property, and allow you to choose whether or not to perform case sensitive comparisons. By default, this function performs case insensitive sorts.
The first argument must be the array containing the objects.
The subsequent argument(s) must be a comma separated list of strings that reference the different object properties to sort by. The last argument (which is optional) is a boolean to choose whether or not to perform case sensitive sorts - use true for case sensitive sorts.
The function will sort each property/key in ascending order by default. If you want a particular key to sort in descending order, then instead pass in an array in this format: ['property_name', true].
Here are some sample uses of the function followed by an explanation (where homes is an array containing the objects):
objSort(homes, 'city') --> sort by city (ascending, case in-sensitive)
objSort(homes, ['city', true]) --> sort by city (descending, case in-sensitive)
objSort(homes, 'city', true) --> sort by city then price (ascending, case sensitive)
objSort(homes, 'city', 'price') --> sort by city then price (both ascending, case in-sensitive)
objSort(homes, 'city', ['price', true]) --> sort by city (ascending) then price (descending), case in-sensitive)
And without further ado, here's the function:
function objSort() {
var args = arguments,
array = args[0],
case_sensitive, keys_length, key, desc, a, b, i;
if (typeof arguments[arguments.length - 1] === 'boolean') {
case_sensitive = arguments[arguments.length - 1];
keys_length = arguments.length - 1;
} else {
case_sensitive = false;
keys_length = arguments.length;
}
return array.sort(function (obj1, obj2) {
for (i = 1; i < keys_length; i++) {
key = args[i];
if (typeof key !== 'string') {
desc = key[1];
key = key[0];
a = obj1[args[i][0]];
b = obj2[args[i][0]];
} else {
desc = false;
a = obj1[args[i]];
b = obj2[args[i]];
}
if (case_sensitive === false && typeof a === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (! desc) {
if (a < b) return -1;
if (a > b) return 1;
} else {
if (a > b) return -1;
if (a < b) return 1;
}
}
return 0;
});
} //end of objSort() function
And here's some sample data:
var homes = [{
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": 162500
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": 1000000
}, {
"h_id": "5",
"city": "new york",
"state": "NY",
"zip": "00010",
"price": 1000000
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "85000",
"price": 300000
}, {
"h_id": "7",
"city": "New York",
"state": "NY",
"zip": "00020",
"price": 345000
}];
A dynamic way to do that with MULTIPLE keys:
filter unique values from each col/key of sort
put in order or reverse it
add weights width zeropad for each object based on indexOf(value) keys values
sort using caclutated weights
Object.defineProperty(Array.prototype, 'orderBy', {
value: function(sorts) {
sorts.map(sort => {
sort.uniques = Array.from(
new Set(this.map(obj => obj[sort.key]))
);
sort.uniques = sort.uniques.sort((a, b) => {
if (typeof a == 'string') {
return sort.inverse ? b.localeCompare(a) : a.localeCompare(b);
}
else if (typeof a == 'number') {
return sort.inverse ? b - a : a - b;
}
else if (typeof a == 'boolean') {
let x = sort.inverse ? (a === b) ? 0 : a? -1 : 1 : (a === b) ? 0 : a? 1 : -1;
return x;
}
return 0;
});
});
const weightOfObject = (obj) => {
let weight = "";
sorts.map(sort => {
let zeropad = `${sort.uniques.length}`.length;
weight += sort.uniques.indexOf(obj[sort.key]).toString().padStart(zeropad, '0');
});
//obj.weight = weight; // if you need to see weights
return weight;
}
this.sort((a, b) => {
return weightOfObject(a).localeCompare( weightOfObject(b) );
});
return this;
}
});
Use:
// works with string, number and boolean
let sortered = your_array.orderBy([
{key: "type", inverse: false},
{key: "title", inverse: false},
{key: "spot", inverse: false},
{key: "internal", inverse: true}
]);
Here's a generic multidimensional sort, allowing for reversing and/or mapping on each level.
Written in Typescript. For Javascript, check out this JSFiddle
The Code
type itemMap = (n: any) => any;
interface SortConfig<T> {
key: keyof T;
reverse?: boolean;
map?: itemMap;
}
export function byObjectValues<T extends object>(keys: ((keyof T) | SortConfig<T>)[]): (a: T, b: T) => 0 | 1 | -1 {
return function(a: T, b: T) {
const firstKey: keyof T | SortConfig<T> = keys[0];
const isSimple = typeof firstKey === 'string';
const key: keyof T = isSimple ? (firstKey as keyof T) : (firstKey as SortConfig<T>).key;
const reverse: boolean = isSimple ? false : !!(firstKey as SortConfig<T>).reverse;
const map: itemMap | null = isSimple ? null : (firstKey as SortConfig<T>).map || null;
const valA = map ? map(a[key]) : a[key];
const valB = map ? map(b[key]) : b[key];
if (valA === valB) {
if (keys.length === 1) {
return 0;
}
return byObjectValues<T>(keys.slice(1))(a, b);
}
if (reverse) {
return valA > valB ? -1 : 1;
}
return valA > valB ? 1 : -1;
};
}
Usage Examples
Sorting a people array by last name, then first name:
interface Person {
firstName: string;
lastName: string;
}
people.sort(byObjectValues<Person>(['lastName','firstName']));
Sort language codes by their name, not their language code (see map), then by descending version (see reverse).
interface Language {
code: string;
version: number;
}
// languageCodeToName(code) is defined elsewhere in code
languageCodes.sort(byObjectValues<Language>([
{
key: 'code',
map(code:string) => languageCodeToName(code),
},
{
key: 'version',
reverse: true,
}
]));
Here's another one that's perhaps closer to your idea for the syntax
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {}; // primers are optional
properties = properties.map(function(prop) {
if( !(prop instanceof Array) ) {
prop = [prop, 'asc']
}
if( prop[1].toLowerCase() == 'desc' ) {
prop[1] = -1;
} else {
prop[1] = 1;
}
return prop;
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}
// just for fun use this to reverse the city name when sorting
function demoPrimer(str) {
return str.split('').reverse().join('');
}
// Example
sortObjects(homes, ['city', ['price', 'desc']], {city: demoPrimer});
Demo: http://jsfiddle.net/Nq4dk/2/
Edit: Just for fun, here's a variation that just takes an sql-like string, so you can do sortObjects(homes, "city, price desc")
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {};
properties = properties.split(/\s*,\s*/).map(function(prop) {
prop = prop.match(/^([^\s]+)(\s*desc)?/i);
if( prop[2] && prop[2].toLowerCase() === 'desc' ) {
return [prop[1] , -1];
} else {
return [prop[1] , 1];
}
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}
Simpler one:
var someArray = [...];
function generateSortFn(props) {
return function (a, b) {
for (var i = 0; i < props.length; i++) {
var prop = props[i];
var name = prop.name;
var reverse = prop.reverse;
if (a[name] < b[name])
return reverse ? 1 : -1;
if (a[name] > b[name])
return reverse ? -1 : 1;
}
return 0;
};
};
someArray.sort(generateSortFn([{name: 'prop1', reverse: true}, {name: 'prop2'}]));
why complicate? just sort it twice! this works perfectly:
(just make sure to reverse the importance order from least to most):
jj.sort( (a, b) => (a.id >= b.id) ? 1 : -1 );
jj.sort( (a, b) => (a.status >= b.status) ? 1 : -1 );
I like SnowBurnt's approach but it needs a tweak to test for equivalence on city NOT a difference.
homes.sort(
function(a,b){
if (a.city==b.city){
return (b.price-a.price);
} else {
return (a.city-b.city);
}
});
Here's my solution based on the Schwartzian transform idiom, hope you find it useful.
function sortByAttribute(array, ...attrs) {
// generate an array of predicate-objects contains
// property getter, and descending indicator
let predicates = attrs.map(pred => {
let descending = pred.charAt(0) === '-' ? -1 : 1;
pred = pred.replace(/^-/, '');
return {
getter: o => o[pred],
descend: descending
};
});
// schwartzian transform idiom implementation. aka: "decorate-sort-undecorate"
return array.map(item => {
return {
src: item,
compareValues: predicates.map(predicate => predicate.getter(item))
};
})
.sort((o1, o2) => {
let i = -1, result = 0;
while (++i < predicates.length) {
if (o1.compareValues[i] < o2.compareValues[i]) result = -1;
if (o1.compareValues[i] > o2.compareValues[i]) result = 1;
if (result *= predicates[i].descend) break;
}
return result;
})
.map(item => item.src);
}
Here's an example how to use it:
let games = [
{ name: 'Pako', rating: 4.21 },
{ name: 'Hill Climb Racing', rating: 3.88 },
{ name: 'Angry Birds Space', rating: 3.88 },
{ name: 'Badland', rating: 4.33 }
];
// sort by one attribute
console.log(sortByAttribute(games, 'name'));
// sort by mupltiple attributes
console.log(sortByAttribute(games, '-rating', 'name'));
Just another option. Consider to use the following utility function:
/** Performs comparing of two items by specified properties
* #param {Array} props for sorting ['name'], ['value', 'city'], ['-date']
* to set descending order on object property just add '-' at the begining of property
*/
export const compareBy = (...props) => (a, b) => {
for (let i = 0; i < props.length; i++) {
const ascValue = props[i].startsWith('-') ? -1 : 1;
const prop = props[i].startsWith('-') ? props[i].substr(1) : props[i];
if (a[prop] !== b[prop]) {
return a[prop] > b[prop] ? ascValue : -ascValue;
}
}
return 0;
};
Example of usage (in your case):
homes.sort(compareBy('city', '-price'));
It should be noted that this function can be even more generalized in order to be able to use nested properties like 'address.city' or 'style.size.width' etc.
Another way
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sortBy(ar) {
return ar.sort((a, b) => a.city === b.city ?
b.price.toString().localeCompare(a.price) :
a.city.toString().localeCompare(b.city));
}
console.log(sortBy(homes));
simply follow the list of your sorting criteria
this code will always remain readable and understandable even if you have 36 sorting criteria to encase
The solution proposed here by Nina is certainly very elegant, but it implies knowing that a value of zero corresponds to a value of false in Boolean logic, and that Boolean tests can return something other than true / false in JavaScript (here are numeric values) which will always be confusing for a beginner.
Also think about who will need to maintain your code. Maybe it would be you: imagine yourself spending your days raking for days the code of another and having a pernicious bug ... and you are exhausted from reading these thousands of lines full of tips
const homes =
[ { h_id: '3', city: 'Dallas', state: 'TX', zip: '75201', price: '162500' }
, { h_id: '4', city: 'Bevery Hills', state: 'CA', zip: '90210', price: '319250' }
, { h_id: '6', city: 'Dallas', state: 'TX', zip: '75000', price: '556699' }
, { h_id: '5', city: 'New York', state: 'NY', zip: '00010', price: '962500' }
]
const fSort = (a,b) =>
{
let Dx = a.city.localeCompare(b.city) // 1st criteria
if (Dx===0) Dx = Number(b.price) - Number(a.price) // 2nd
// if (Dx===0) Dx = ... // 3rd
// if (Dx===0) Dx = ... // 4th....
return Dx
}
console.log( homes.sort(fSort))
Adding a couple helper functions lets you solved this kind of problem generically and simply. sortByKey takes an array and a function which should return a list of items with which to compare each array entry.
This takes advantage of the fact that javascript does smart comparison of arrays of simple values, with [2] < [2, 0] < [2, 1] < [10, 0].
// Two helpers:
function cmp(a, b) {
if (a > b) {
return 1
} else if (a < b) {
return -1
} else {
return 0
}
}
function sortByKey(arr, key) {
arr.sort((a, b) => cmp(key(a), key(b)))
}
// A demonstration:
let arr = [{a:1, b:2}, {b:3, a:0}, {a:1, b:1}, {a:2, b:2}, {a:2, b:1}, {a:1, b:10}]
sortByKey(arr, item => [item.a, item.b])
console.log(JSON.stringify(arr))
// '[{"b":3,"a":0},{"a":1,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":1},{"a":2,"b":2}]'
sortByKey(arr, item => [item.b, item.a])
console.log(JSON.stringify(arr))
// '[{"a":1,"b":1},{"a":2,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":2},{"b":3,"a":0}]'
I've lovingly stolen this idea from Python's list.sort function.
To make things simple, use these helper functions.
You can sort by as many fields as you need. For each sort field, specify the property name, and then, optionally, specify -1 as the sort direction to sort descending instead of ascending.
const data = [
{"h_id":"3","city":"Dallas","state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills","state":"CA","zip":"90210","price":"319250"},
{"h_id":"6","city":"Dallas","state":"TX","zip":"75000","price":"556699"},
{"h_id":"5","city":"New York","state":"NY","zip":"00010","price":"962500"},
{"h_id":"7","city":"New York","state":"NY","zip":"00010","price":"800500"}
]
const sortLexically = (p,d=1)=>(a,b)=>d * a[p].localeCompare(b[p])
const sortNumerically = (p,d=1)=>(a,b)=>d * (a[p]-b[p])
const sortBy = sorts=>(a,b)=>sorts.reduce((r,s)=>r||s(a,b),0)
// sort first by city, then by price descending
data.sort(sortBy([sortLexically('city'), sortNumerically('price', -1)]))
console.log(data)
Here is a generic version of #Snowburnt's solution:
var sortarray = [{field:'city', direction:'asc'}, {field:'price', direction:'desc'}];
array.sort(function(a,b){
for(var i=0; i<sortarray.length; i++){
retval = a[sortarray[i].field] < b[sortarray[i].field] ? -1 : a[sortarray[i].field] > b[sortarray[i].field] ? 1 : 0;
if (sortarray[i].direction == "desc") {
retval = retval * -1;
}
if (retval !== 0) {
return retval;
}
}
}
})
This is based on a sort routine I'm using. I didn't test this specific code so it may have errors but you get the idea. The idea is to sort based on the first field that indicates a difference and then stop and go to the next record. So, if you're sorting by three fields and the first field in the compare is enough to determine the sort order of the two records being sorted then return that sort result and go to the next record.
I tested it (actually with a little more complex sort logic) on 5000 records and it did it in the blink of an eye. If you're actually loading more than 1000 records to the client you should probably be using sever-side sorting and filtering.
This code isn't handling case-sensitivity but I leave it to the reader to handle this trivial modification.
function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
Using :
sort(homes, [{city : 'asc'}, {price: 'desc'}])
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
console.log(sort(homes, [{city : 'asc'}, {price: 'desc'}]));
// custom sorting by city
const sortArray = ['Dallas', 'New York', 'Beverly Hills'];
const sortData = (sortBy) =>
data
.sort((a, b) => {
const aIndex = sortBy.indexOf(a.city);
const bIndex = sortBy.indexOf(b.city);
if (aIndex < bIndex) {
return -1;
}
if (aIndex === bIndex) {
// price descending
return b.price- a.price;
}
return 1;
});
sortData(sortArray);
You can use lodash orderBy function lodash
It takes two params array of fields, and array of directions ('asc','desc')
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
var sorted =. data._.orderBy(data, ['city', 'price'], ['asc','desc'])
A very intuitive functional solution can be crafted by adding 3 relatively simple helpers. Before we dive in, let's start with the usage:
function usage(homes, { asc, desc, fallback }) {
homes.sort(fallback(
asc(home => home.city),
desc(home => parseInt(home.price, 10)),
));
console.log(homes);
}
var homes = [{
h_id: "3",
city: "Dallas",
state: "TX",
zip: "75201",
price: "162500",
}, {
h_id: "4",
city: "Bevery Hills",
state: "CA",
zip: "90210",
price: "319250",
}, {
h_id: "6",
city: "Dallas",
state: "TX",
zip: "75000",
price: "556699",
}, {
h_id: "5",
city: "New York",
state: "NY",
zip: "00010",
price: "962500",
}];
const SortHelpers = (function () {
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
return { asc, desc, fallback };
})();
usage(homes, SortHelpers);
If you scrolled down the snippet you probably already saw the helpers:
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
Let me quickly explain what each of these functions does.
asc creates a comparator function. The provided function fn is called for both the comparator arguments a and b. The results of the two function calls are then compared. -1 is returned if resultA < resultB, 1 is returned if resultA > resultB, or 0 otherwise. These return values correspond with an ascending order direction.
It could also be written like this:
function asc(fn) {
return function (a, b) {
// apply `fn` to both `a` and `b`
a = fn(a);
b = fn(b);
if (a < b) return -1;
if (a > b) return 1;
return 0;
// or `return -(a < b) || +(a > b)` for short
};
}
desc is super simple, since it just calls asc but swaps the a and b arguments, resulting in descending order instead of ascending.
fallback (there might be a better name for this) allows us to use multiple comparator functions with a single sort.
Both asc and desc can be passed to sort by themself.
homes.sort(asc(home => home.city))
There is however an issue if you want to combine multiple comparator functions. sort only accepts a single comparator function. fallback combines multiple comparator functions into a single comparator.
The first comparator is called with arguments a and b, if the comparator returns the value 0 (meaning that the values are equal) then we fall back to the next comparator. This continues until a non-0 value is found, or until all comparators are called, in which case the return value is 0.
You can provide your custom comparator functions to fallback() as well. Say you want to use localeCompare() instead of comparing strings with < and >. In such a case you can replace asc(home => home.city) with (a, b) => a.city.localeCompare(b.city).
homes.sort(fallback(
(a, b) => a.city.localeCompare(b.city),
desc(home => parseInt(home.price, 10)),
));
One thing to note is that values that can be undefined will always return false when comparing with < and >. So if a value can be missing you might want to sort by its presence first.
homes.sort(fallback(
// homes with optionalProperty first, true (1) > false (0) so we use desc
desc(home => home.optionalProperty != null), // checks for both null and undefined
asc(home => home.optionalProperty),
// ...
))
Since comparing strings with localeCompare() is such a common thing to do, you could include this as part of asc().
function hasMethod(item, methodName) {
return item != null && typeof item[methodName] === "function";
}
function asc(fn) {
return function (a, b) {
a = fn(a);
b = fn(b);
const areLocaleComparable =
hasMethod(a, "localeCompare") && hasMethod(b, "localeCompare");
if (areLocaleComparable) return a.localeCompare(b);
return -(a < b) || +(a > b);
};
}
Here, you can try the smaller and convenient way to sort by multiple fields!
var homes = [
{ "h_id": "3", "city": "Dallas", "state": "TX", "zip": "75201", "price": "162500" },
{ "h_id": "4", "city": "Bevery Hills", "state": "CA", "zip": "90210", "price": "319250" },
{ "h_id": "6", "city": "Dallas", "state": "TX", "zip": "75000", "price": "556699" },
{ "h_id": "5", "city": "New York", "state": "NY", "zip": "00010", "price": "962500" }
];
homes.sort((a, b)=> {
if (a.city === b.city){
return a.price < b.price ? -1 : 1
} else {
return a.city < b.city ? -1 : 1
}
})
console.log(homes);
function sortMultiFields(prop){
return function(a,b){
for(i=0;i<prop.length;i++)
{
var reg = /^\d+$/;
var x=1;
var field1=prop[i];
if(prop[i].indexOf("-")==0)
{
field1=prop[i].substr(1,prop[i].length);
x=-x;
}
if(reg.test(a[field1]))
{
a[field1]=parseFloat(a[field1]);
b[field1]=parseFloat(b[field1]);
}
if( a[field1] > b[field1])
return x;
else if(a[field1] < b[field1])
return -x;
}
}
}
How to use (put -(minus) sign before field if you want to sort in descending order particular field)
homes.sort(sortMultiFields(["city","-price"]));
Using above function you can sort any json array with multiple fields. No need to change function body at all
Adaptation of #chriskelly 's answer.
Most answers overlook that price will not sort properly if the value is in the ten thousands and lower or over a million. The resaon being JS sorts alphabetically. It was answered pretty well here, Why can't JavaScript sort "5, 10, 1" and here How to sort an array of integers correctly.
Ultimately we have to do some evaluation if the field or node we're sorting by is an number. I am not saying that using parseInt() in this case is the correct answer, the sorted results are more important.
var homes = [{
"h_id": "2",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62500"
}, {
"h_id": "1",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62510"
}, {
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "162500"
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": "319250"
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "75000",
"price": "556699"
}, {
"h_id": "5",
"city": "New York",
"state": "NY",
"zip": "00010",
"price": "962500"
}];
homes.sort(fieldSorter(['price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function(a, b) {
return fields
.map(function(o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o = o.substring(1);
}
if (!parseInt(a[o]) && !parseInt(b[o])) {
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
} else {
return dir > 0 ? a[o] - b[o] : b[o] - a[o];
}
})
.reduce(function firstNonZeroValue(p, n) {
return p ? p : n;
}, 0);
};
}
document.getElementById("output").innerHTML = '<pre>' + JSON.stringify(homes, null, '\t') + '</pre>';
<div id="output">
</div>
A fiddle to test with
Wow, there are some complex solutions here. So complex I decided to come up with something simpler but also quite powerful. Here it is;
function sortByPriority(data, priorities) {
if (priorities.length == 0) {
return data;
}
const nextPriority = priorities[0];
const remainingPriorities = priorities.slice(1);
const matched = data.filter(item => item.hasOwnProperty(nextPriority));
const remainingData = data.filter(item => !item.hasOwnProperty(nextPriority));
return sortByPriority(matched, remainingPriorities)
.sort((a, b) => (a[nextPriority] > b[nextPriority]) ? 1 : -1)
.concat(sortByPriority(remainingData, remainingPriorities));
}
And here is an example of how you use it.
const data = [
{ id: 1, mediumPriority: 'bbb', lowestPriority: 'ggg' },
{ id: 2, highestPriority: 'bbb', mediumPriority: 'ccc', lowestPriority: 'ggg' },
{ id: 3, mediumPriority: 'aaa', lowestPriority: 'ggg' },
];
const priorities = [
'highestPriority',
'mediumPriority',
'lowestPriority'
];
const sorted = sortByPriority(data, priorities);
This will first sort by the precedence of the attributes, then by the value of the attributes.
I think this may be the easiest way to do it.
https://coderwall.com/p/ebqhca/javascript-sort-by-two-fields
It's really simple and I tried it with 3 different key value pairs and it worked great.
Here is a simple example, look at the link for more details
testSort(data) {
return data.sort(
a['nameOne'] > b['nameOne'] ? 1
: b['nameOne'] > a['nameOne'] ? -1 : 0 ||
a['date'] > b['date'] ||
a['number'] - b['number']
);
}

sort function and forget previous result [duplicate]

From this original question, how would I apply a sort on multiple fields?
Using this slightly adapted structure, how would I sort city (ascending) & then price (descending)?
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
I liked the fact than an answer was given which provided a general approach. Where I plan to use this code, I will have to sort dates as well as other things. The ability to "prime" the object seemed handy, if not a little cumbersome.
I've tried to build this answer into a nice generic example, but I'm not having much luck.
You could use a chained sorting approach by taking the delta of values until it reaches a value not equal to zero.
var data = [{ h_id: "3", city: "Dallas", state: "TX", zip: "75201", price: "162500" }, { h_id: "4", city: "Bevery Hills", state: "CA", zip: "90210", price: "319250" }, { h_id: "6", city: "Dallas", state: "TX", zip: "75000", price: "556699" }, { h_id: "5", city: "New York", state: "NY", zip: "00010", price: "962500" }];
data.sort(function (a, b) {
return a.city.localeCompare(b.city) || b.price - a.price;
});
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Or, using es6, simply:
data.sort((a, b) => a.city.localeCompare(b.city) || b.price - a.price);
for a non-generic, simple solution to your exact problem:
homes.sort(
function(a, b) {
if (a.city === b.city) {
// Price is only important when cities are the same
return b.price - a.price;
}
return a.city > b.city ? 1 : -1;
});
A multi dimensional sorting method, based on this answer:
Update: Here is an "optimized" version. It does a lot more preprocessing and creates a comparison function for each sorting option beforehand. It might need more more memory (as it stores a function for each sorting option, but it should preform a bit better as it does not have to determine the correct settings during the comparison. I have not done any profiling though.
var sort_by;
(function() {
// utility functions
var default_cmp = function(a, b) {
if (a == b) return 0;
return a < b ? -1 : 1;
},
getCmpFunc = function(primer, reverse) {
var dfc = default_cmp, // closer in scope
cmp = default_cmp;
if (primer) {
cmp = function(a, b) {
return dfc(primer(a), primer(b));
};
}
if (reverse) {
return function(a, b) {
return -1 * cmp(a, b);
};
}
return cmp;
};
// actual implementation
sort_by = function() {
var fields = [],
n_fields = arguments.length,
field, name, reverse, cmp;
// preprocess sorting options
for (var i = 0; i < n_fields; i++) {
field = arguments[i];
if (typeof field === 'string') {
name = field;
cmp = default_cmp;
}
else {
name = field.name;
cmp = getCmpFunc(field.primer, field.reverse);
}
fields.push({
name: name,
cmp: cmp
});
}
// final comparison function
return function(A, B) {
var a, b, name, result;
for (var i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
name = field.name;
result = field.cmp(A[name], B[name]);
if (result !== 0) break;
}
return result;
}
}
}());
Example usage:
homes.sort(sort_by('city', {name:'price', primer: parseInt, reverse: true}));
DEMO
Original function:
var sort_by = function() {
var fields = [].slice.call(arguments),
n_fields = fields.length;
return function(A,B) {
var a, b, field, key, primer, reverse, result, i;
for(i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
key = typeof field === 'string' ? field : field.name;
a = A[key];
b = B[key];
if (typeof field.primer !== 'undefined'){
a = field.primer(a);
b = field.primer(b);
}
reverse = (field.reverse) ? -1 : 1;
if (a<b) result = reverse * -1;
if (a>b) result = reverse * 1;
if(result !== 0) break;
}
return result;
}
};
DEMO
Here is a simple functional generic approach. Specify sort order using array. Prepend minus to specify descending order.
var homes = [
{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":"319250"},
{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":"556699"},
{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":"962500"}
];
homes.sort(fieldSorter(['city', '-price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function (a, b) {
return fields
.map(function (o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o=o.substring(1);
}
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
})
.reduce(function firstNonZeroValue (p,n) {
return p ? p : n;
}, 0);
};
}
Edit: in ES6 it's even shorter!
"use strict";
const fieldSorter = (fields) => (a, b) => fields.map(o => {
let dir = 1;
if (o[0] === '-') { dir = -1; o=o.substring(1); }
return a[o] > b[o] ? dir : a[o] < b[o] ? -(dir) : 0;
}).reduce((p, n) => p ? p : n, 0);
const homes = [{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":162500}, {"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":319250},{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":556699},{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":962500}];
const sortedHomes = homes.sort(fieldSorter(['state', '-price']));
document.write('<pre>' + JSON.stringify(sortedHomes, null, '\t') + '</pre>')
I made a quite generic multi feature sorter today. You can have a look at thenBy.js here: https://github.com/Teun/thenBy.js
It allows you to use the standard Array.sort, but with firstBy().thenBy().thenBy() style. It is way less code and complexity than the solutions posted above.
Here's an extensible way to sort by multiple fields.
homes.sort(function(left, right) {
var city_order = left.city.localeCompare(right.city);
var price_order = parseInt(left.price) - parseInt(right.price);
return city_order || -price_order;
});
Notes
A function passed to array sort is expected to return negative, zero, positive to indicate less, equal, greater.
a.localeCompare(b) is universally supported for strings, and returns -1,0,1 if a<b,a==b,a>b.
Subtraction works on numeric fields, because a - b gives -,0,+ if a<b,a==b,a>b.
|| in the last line gives city priority over price.
Negate to reverse order in any field, as in -price_order
Add new fields to the or-chain: return city_order || -price_order || date_order;
Date compare with subtraction, because date math converts to milliseconds since 1970.var date_order = new Date(left.date) - new Date(right.date);
Boolean compare with subtraction, which is guaranteed to turn true and false to 1 and 0 (therefore the subtraction produces -1 or 0 or 1). var goodness_order = Boolean(left.is_good) - Boolean(right.is_good)This is unusual enough that I'd suggest drawing attention with the Boolean constructor, even if they're already boolean.
This is a complete cheat but I think that it adds value to this question because it's basically a canned library function that you can use out-of-the box.
If your code has access to lodash or a lodash compatible library like underscore then you can use the _.sortBy method. The snippet below is copied directly from the lodash documentation.
The commented results in the examples looks like they return arrays of arrays but that's just showing the order and not the actual results which are an array of objects.
var users = [
{ 'user': 'fred',   'age': 48 },
{ 'user': 'barney', 'age': 36 },
{ 'user': 'fred',   'age': 40 },
{ 'user': 'barney', 'age': 34 }
];
_.sortBy(users, [function(o) { return o.user; }]);
// => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]
_.sortBy(users, ['user', 'age']);
// => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]]
The following function will allow you to sort an array of objects on one or multiple properties, either ascending (default) or descending on each property, and allow you to choose whether or not to perform case sensitive comparisons. By default, this function performs case insensitive sorts.
The first argument must be the array containing the objects.
The subsequent argument(s) must be a comma separated list of strings that reference the different object properties to sort by. The last argument (which is optional) is a boolean to choose whether or not to perform case sensitive sorts - use true for case sensitive sorts.
The function will sort each property/key in ascending order by default. If you want a particular key to sort in descending order, then instead pass in an array in this format: ['property_name', true].
Here are some sample uses of the function followed by an explanation (where homes is an array containing the objects):
objSort(homes, 'city') --> sort by city (ascending, case in-sensitive)
objSort(homes, ['city', true]) --> sort by city (descending, case in-sensitive)
objSort(homes, 'city', true) --> sort by city then price (ascending, case sensitive)
objSort(homes, 'city', 'price') --> sort by city then price (both ascending, case in-sensitive)
objSort(homes, 'city', ['price', true]) --> sort by city (ascending) then price (descending), case in-sensitive)
And without further ado, here's the function:
function objSort() {
var args = arguments,
array = args[0],
case_sensitive, keys_length, key, desc, a, b, i;
if (typeof arguments[arguments.length - 1] === 'boolean') {
case_sensitive = arguments[arguments.length - 1];
keys_length = arguments.length - 1;
} else {
case_sensitive = false;
keys_length = arguments.length;
}
return array.sort(function (obj1, obj2) {
for (i = 1; i < keys_length; i++) {
key = args[i];
if (typeof key !== 'string') {
desc = key[1];
key = key[0];
a = obj1[args[i][0]];
b = obj2[args[i][0]];
} else {
desc = false;
a = obj1[args[i]];
b = obj2[args[i]];
}
if (case_sensitive === false && typeof a === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (! desc) {
if (a < b) return -1;
if (a > b) return 1;
} else {
if (a > b) return -1;
if (a < b) return 1;
}
}
return 0;
});
} //end of objSort() function
And here's some sample data:
var homes = [{
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": 162500
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": 1000000
}, {
"h_id": "5",
"city": "new york",
"state": "NY",
"zip": "00010",
"price": 1000000
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "85000",
"price": 300000
}, {
"h_id": "7",
"city": "New York",
"state": "NY",
"zip": "00020",
"price": 345000
}];
A dynamic way to do that with MULTIPLE keys:
filter unique values from each col/key of sort
put in order or reverse it
add weights width zeropad for each object based on indexOf(value) keys values
sort using caclutated weights
Object.defineProperty(Array.prototype, 'orderBy', {
value: function(sorts) {
sorts.map(sort => {
sort.uniques = Array.from(
new Set(this.map(obj => obj[sort.key]))
);
sort.uniques = sort.uniques.sort((a, b) => {
if (typeof a == 'string') {
return sort.inverse ? b.localeCompare(a) : a.localeCompare(b);
}
else if (typeof a == 'number') {
return sort.inverse ? b - a : a - b;
}
else if (typeof a == 'boolean') {
let x = sort.inverse ? (a === b) ? 0 : a? -1 : 1 : (a === b) ? 0 : a? 1 : -1;
return x;
}
return 0;
});
});
const weightOfObject = (obj) => {
let weight = "";
sorts.map(sort => {
let zeropad = `${sort.uniques.length}`.length;
weight += sort.uniques.indexOf(obj[sort.key]).toString().padStart(zeropad, '0');
});
//obj.weight = weight; // if you need to see weights
return weight;
}
this.sort((a, b) => {
return weightOfObject(a).localeCompare( weightOfObject(b) );
});
return this;
}
});
Use:
// works with string, number and boolean
let sortered = your_array.orderBy([
{key: "type", inverse: false},
{key: "title", inverse: false},
{key: "spot", inverse: false},
{key: "internal", inverse: true}
]);
Here's a generic multidimensional sort, allowing for reversing and/or mapping on each level.
Written in Typescript. For Javascript, check out this JSFiddle
The Code
type itemMap = (n: any) => any;
interface SortConfig<T> {
key: keyof T;
reverse?: boolean;
map?: itemMap;
}
export function byObjectValues<T extends object>(keys: ((keyof T) | SortConfig<T>)[]): (a: T, b: T) => 0 | 1 | -1 {
return function(a: T, b: T) {
const firstKey: keyof T | SortConfig<T> = keys[0];
const isSimple = typeof firstKey === 'string';
const key: keyof T = isSimple ? (firstKey as keyof T) : (firstKey as SortConfig<T>).key;
const reverse: boolean = isSimple ? false : !!(firstKey as SortConfig<T>).reverse;
const map: itemMap | null = isSimple ? null : (firstKey as SortConfig<T>).map || null;
const valA = map ? map(a[key]) : a[key];
const valB = map ? map(b[key]) : b[key];
if (valA === valB) {
if (keys.length === 1) {
return 0;
}
return byObjectValues<T>(keys.slice(1))(a, b);
}
if (reverse) {
return valA > valB ? -1 : 1;
}
return valA > valB ? 1 : -1;
};
}
Usage Examples
Sorting a people array by last name, then first name:
interface Person {
firstName: string;
lastName: string;
}
people.sort(byObjectValues<Person>(['lastName','firstName']));
Sort language codes by their name, not their language code (see map), then by descending version (see reverse).
interface Language {
code: string;
version: number;
}
// languageCodeToName(code) is defined elsewhere in code
languageCodes.sort(byObjectValues<Language>([
{
key: 'code',
map(code:string) => languageCodeToName(code),
},
{
key: 'version',
reverse: true,
}
]));
Here's another one that's perhaps closer to your idea for the syntax
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {}; // primers are optional
properties = properties.map(function(prop) {
if( !(prop instanceof Array) ) {
prop = [prop, 'asc']
}
if( prop[1].toLowerCase() == 'desc' ) {
prop[1] = -1;
} else {
prop[1] = 1;
}
return prop;
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}
// just for fun use this to reverse the city name when sorting
function demoPrimer(str) {
return str.split('').reverse().join('');
}
// Example
sortObjects(homes, ['city', ['price', 'desc']], {city: demoPrimer});
Demo: http://jsfiddle.net/Nq4dk/2/
Edit: Just for fun, here's a variation that just takes an sql-like string, so you can do sortObjects(homes, "city, price desc")
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {};
properties = properties.split(/\s*,\s*/).map(function(prop) {
prop = prop.match(/^([^\s]+)(\s*desc)?/i);
if( prop[2] && prop[2].toLowerCase() === 'desc' ) {
return [prop[1] , -1];
} else {
return [prop[1] , 1];
}
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}
Simpler one:
var someArray = [...];
function generateSortFn(props) {
return function (a, b) {
for (var i = 0; i < props.length; i++) {
var prop = props[i];
var name = prop.name;
var reverse = prop.reverse;
if (a[name] < b[name])
return reverse ? 1 : -1;
if (a[name] > b[name])
return reverse ? -1 : 1;
}
return 0;
};
};
someArray.sort(generateSortFn([{name: 'prop1', reverse: true}, {name: 'prop2'}]));
why complicate? just sort it twice! this works perfectly:
(just make sure to reverse the importance order from least to most):
jj.sort( (a, b) => (a.id >= b.id) ? 1 : -1 );
jj.sort( (a, b) => (a.status >= b.status) ? 1 : -1 );
I like SnowBurnt's approach but it needs a tweak to test for equivalence on city NOT a difference.
homes.sort(
function(a,b){
if (a.city==b.city){
return (b.price-a.price);
} else {
return (a.city-b.city);
}
});
Here's my solution based on the Schwartzian transform idiom, hope you find it useful.
function sortByAttribute(array, ...attrs) {
// generate an array of predicate-objects contains
// property getter, and descending indicator
let predicates = attrs.map(pred => {
let descending = pred.charAt(0) === '-' ? -1 : 1;
pred = pred.replace(/^-/, '');
return {
getter: o => o[pred],
descend: descending
};
});
// schwartzian transform idiom implementation. aka: "decorate-sort-undecorate"
return array.map(item => {
return {
src: item,
compareValues: predicates.map(predicate => predicate.getter(item))
};
})
.sort((o1, o2) => {
let i = -1, result = 0;
while (++i < predicates.length) {
if (o1.compareValues[i] < o2.compareValues[i]) result = -1;
if (o1.compareValues[i] > o2.compareValues[i]) result = 1;
if (result *= predicates[i].descend) break;
}
return result;
})
.map(item => item.src);
}
Here's an example how to use it:
let games = [
{ name: 'Pako', rating: 4.21 },
{ name: 'Hill Climb Racing', rating: 3.88 },
{ name: 'Angry Birds Space', rating: 3.88 },
{ name: 'Badland', rating: 4.33 }
];
// sort by one attribute
console.log(sortByAttribute(games, 'name'));
// sort by mupltiple attributes
console.log(sortByAttribute(games, '-rating', 'name'));
Just another option. Consider to use the following utility function:
/** Performs comparing of two items by specified properties
* #param {Array} props for sorting ['name'], ['value', 'city'], ['-date']
* to set descending order on object property just add '-' at the begining of property
*/
export const compareBy = (...props) => (a, b) => {
for (let i = 0; i < props.length; i++) {
const ascValue = props[i].startsWith('-') ? -1 : 1;
const prop = props[i].startsWith('-') ? props[i].substr(1) : props[i];
if (a[prop] !== b[prop]) {
return a[prop] > b[prop] ? ascValue : -ascValue;
}
}
return 0;
};
Example of usage (in your case):
homes.sort(compareBy('city', '-price'));
It should be noted that this function can be even more generalized in order to be able to use nested properties like 'address.city' or 'style.size.width' etc.
Another way
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sortBy(ar) {
return ar.sort((a, b) => a.city === b.city ?
b.price.toString().localeCompare(a.price) :
a.city.toString().localeCompare(b.city));
}
console.log(sortBy(homes));
simply follow the list of your sorting criteria
this code will always remain readable and understandable even if you have 36 sorting criteria to encase
The solution proposed here by Nina is certainly very elegant, but it implies knowing that a value of zero corresponds to a value of false in Boolean logic, and that Boolean tests can return something other than true / false in JavaScript (here are numeric values) which will always be confusing for a beginner.
Also think about who will need to maintain your code. Maybe it would be you: imagine yourself spending your days raking for days the code of another and having a pernicious bug ... and you are exhausted from reading these thousands of lines full of tips
const homes =
[ { h_id: '3', city: 'Dallas', state: 'TX', zip: '75201', price: '162500' }
, { h_id: '4', city: 'Bevery Hills', state: 'CA', zip: '90210', price: '319250' }
, { h_id: '6', city: 'Dallas', state: 'TX', zip: '75000', price: '556699' }
, { h_id: '5', city: 'New York', state: 'NY', zip: '00010', price: '962500' }
]
const fSort = (a,b) =>
{
let Dx = a.city.localeCompare(b.city) // 1st criteria
if (Dx===0) Dx = Number(b.price) - Number(a.price) // 2nd
// if (Dx===0) Dx = ... // 3rd
// if (Dx===0) Dx = ... // 4th....
return Dx
}
console.log( homes.sort(fSort))
Adding a couple helper functions lets you solved this kind of problem generically and simply. sortByKey takes an array and a function which should return a list of items with which to compare each array entry.
This takes advantage of the fact that javascript does smart comparison of arrays of simple values, with [2] < [2, 0] < [2, 1] < [10, 0].
// Two helpers:
function cmp(a, b) {
if (a > b) {
return 1
} else if (a < b) {
return -1
} else {
return 0
}
}
function sortByKey(arr, key) {
arr.sort((a, b) => cmp(key(a), key(b)))
}
// A demonstration:
let arr = [{a:1, b:2}, {b:3, a:0}, {a:1, b:1}, {a:2, b:2}, {a:2, b:1}, {a:1, b:10}]
sortByKey(arr, item => [item.a, item.b])
console.log(JSON.stringify(arr))
// '[{"b":3,"a":0},{"a":1,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":1},{"a":2,"b":2}]'
sortByKey(arr, item => [item.b, item.a])
console.log(JSON.stringify(arr))
// '[{"a":1,"b":1},{"a":2,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":2},{"b":3,"a":0}]'
I've lovingly stolen this idea from Python's list.sort function.
To make things simple, use these helper functions.
You can sort by as many fields as you need. For each sort field, specify the property name, and then, optionally, specify -1 as the sort direction to sort descending instead of ascending.
const data = [
{"h_id":"3","city":"Dallas","state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills","state":"CA","zip":"90210","price":"319250"},
{"h_id":"6","city":"Dallas","state":"TX","zip":"75000","price":"556699"},
{"h_id":"5","city":"New York","state":"NY","zip":"00010","price":"962500"},
{"h_id":"7","city":"New York","state":"NY","zip":"00010","price":"800500"}
]
const sortLexically = (p,d=1)=>(a,b)=>d * a[p].localeCompare(b[p])
const sortNumerically = (p,d=1)=>(a,b)=>d * (a[p]-b[p])
const sortBy = sorts=>(a,b)=>sorts.reduce((r,s)=>r||s(a,b),0)
// sort first by city, then by price descending
data.sort(sortBy([sortLexically('city'), sortNumerically('price', -1)]))
console.log(data)
Here is a generic version of #Snowburnt's solution:
var sortarray = [{field:'city', direction:'asc'}, {field:'price', direction:'desc'}];
array.sort(function(a,b){
for(var i=0; i<sortarray.length; i++){
retval = a[sortarray[i].field] < b[sortarray[i].field] ? -1 : a[sortarray[i].field] > b[sortarray[i].field] ? 1 : 0;
if (sortarray[i].direction == "desc") {
retval = retval * -1;
}
if (retval !== 0) {
return retval;
}
}
}
})
This is based on a sort routine I'm using. I didn't test this specific code so it may have errors but you get the idea. The idea is to sort based on the first field that indicates a difference and then stop and go to the next record. So, if you're sorting by three fields and the first field in the compare is enough to determine the sort order of the two records being sorted then return that sort result and go to the next record.
I tested it (actually with a little more complex sort logic) on 5000 records and it did it in the blink of an eye. If you're actually loading more than 1000 records to the client you should probably be using sever-side sorting and filtering.
This code isn't handling case-sensitivity but I leave it to the reader to handle this trivial modification.
function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
Using :
sort(homes, [{city : 'asc'}, {price: 'desc'}])
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
console.log(sort(homes, [{city : 'asc'}, {price: 'desc'}]));
// custom sorting by city
const sortArray = ['Dallas', 'New York', 'Beverly Hills'];
const sortData = (sortBy) =>
data
.sort((a, b) => {
const aIndex = sortBy.indexOf(a.city);
const bIndex = sortBy.indexOf(b.city);
if (aIndex < bIndex) {
return -1;
}
if (aIndex === bIndex) {
// price descending
return b.price- a.price;
}
return 1;
});
sortData(sortArray);
You can use lodash orderBy function lodash
It takes two params array of fields, and array of directions ('asc','desc')
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
var sorted =. data._.orderBy(data, ['city', 'price'], ['asc','desc'])
A very intuitive functional solution can be crafted by adding 3 relatively simple helpers. Before we dive in, let's start with the usage:
function usage(homes, { asc, desc, fallback }) {
homes.sort(fallback(
asc(home => home.city),
desc(home => parseInt(home.price, 10)),
));
console.log(homes);
}
var homes = [{
h_id: "3",
city: "Dallas",
state: "TX",
zip: "75201",
price: "162500",
}, {
h_id: "4",
city: "Bevery Hills",
state: "CA",
zip: "90210",
price: "319250",
}, {
h_id: "6",
city: "Dallas",
state: "TX",
zip: "75000",
price: "556699",
}, {
h_id: "5",
city: "New York",
state: "NY",
zip: "00010",
price: "962500",
}];
const SortHelpers = (function () {
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
return { asc, desc, fallback };
})();
usage(homes, SortHelpers);
If you scrolled down the snippet you probably already saw the helpers:
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
Let me quickly explain what each of these functions does.
asc creates a comparator function. The provided function fn is called for both the comparator arguments a and b. The results of the two function calls are then compared. -1 is returned if resultA < resultB, 1 is returned if resultA > resultB, or 0 otherwise. These return values correspond with an ascending order direction.
It could also be written like this:
function asc(fn) {
return function (a, b) {
// apply `fn` to both `a` and `b`
a = fn(a);
b = fn(b);
if (a < b) return -1;
if (a > b) return 1;
return 0;
// or `return -(a < b) || +(a > b)` for short
};
}
desc is super simple, since it just calls asc but swaps the a and b arguments, resulting in descending order instead of ascending.
fallback (there might be a better name for this) allows us to use multiple comparator functions with a single sort.
Both asc and desc can be passed to sort by themself.
homes.sort(asc(home => home.city))
There is however an issue if you want to combine multiple comparator functions. sort only accepts a single comparator function. fallback combines multiple comparator functions into a single comparator.
The first comparator is called with arguments a and b, if the comparator returns the value 0 (meaning that the values are equal) then we fall back to the next comparator. This continues until a non-0 value is found, or until all comparators are called, in which case the return value is 0.
You can provide your custom comparator functions to fallback() as well. Say you want to use localeCompare() instead of comparing strings with < and >. In such a case you can replace asc(home => home.city) with (a, b) => a.city.localeCompare(b.city).
homes.sort(fallback(
(a, b) => a.city.localeCompare(b.city),
desc(home => parseInt(home.price, 10)),
));
One thing to note is that values that can be undefined will always return false when comparing with < and >. So if a value can be missing you might want to sort by its presence first.
homes.sort(fallback(
// homes with optionalProperty first, true (1) > false (0) so we use desc
desc(home => home.optionalProperty != null), // checks for both null and undefined
asc(home => home.optionalProperty),
// ...
))
Since comparing strings with localeCompare() is such a common thing to do, you could include this as part of asc().
function hasMethod(item, methodName) {
return item != null && typeof item[methodName] === "function";
}
function asc(fn) {
return function (a, b) {
a = fn(a);
b = fn(b);
const areLocaleComparable =
hasMethod(a, "localeCompare") && hasMethod(b, "localeCompare");
if (areLocaleComparable) return a.localeCompare(b);
return -(a < b) || +(a > b);
};
}
Here, you can try the smaller and convenient way to sort by multiple fields!
var homes = [
{ "h_id": "3", "city": "Dallas", "state": "TX", "zip": "75201", "price": "162500" },
{ "h_id": "4", "city": "Bevery Hills", "state": "CA", "zip": "90210", "price": "319250" },
{ "h_id": "6", "city": "Dallas", "state": "TX", "zip": "75000", "price": "556699" },
{ "h_id": "5", "city": "New York", "state": "NY", "zip": "00010", "price": "962500" }
];
homes.sort((a, b)=> {
if (a.city === b.city){
return a.price < b.price ? -1 : 1
} else {
return a.city < b.city ? -1 : 1
}
})
console.log(homes);
function sortMultiFields(prop){
return function(a,b){
for(i=0;i<prop.length;i++)
{
var reg = /^\d+$/;
var x=1;
var field1=prop[i];
if(prop[i].indexOf("-")==0)
{
field1=prop[i].substr(1,prop[i].length);
x=-x;
}
if(reg.test(a[field1]))
{
a[field1]=parseFloat(a[field1]);
b[field1]=parseFloat(b[field1]);
}
if( a[field1] > b[field1])
return x;
else if(a[field1] < b[field1])
return -x;
}
}
}
How to use (put -(minus) sign before field if you want to sort in descending order particular field)
homes.sort(sortMultiFields(["city","-price"]));
Using above function you can sort any json array with multiple fields. No need to change function body at all
Adaptation of #chriskelly 's answer.
Most answers overlook that price will not sort properly if the value is in the ten thousands and lower or over a million. The resaon being JS sorts alphabetically. It was answered pretty well here, Why can't JavaScript sort "5, 10, 1" and here How to sort an array of integers correctly.
Ultimately we have to do some evaluation if the field or node we're sorting by is an number. I am not saying that using parseInt() in this case is the correct answer, the sorted results are more important.
var homes = [{
"h_id": "2",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62500"
}, {
"h_id": "1",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62510"
}, {
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "162500"
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": "319250"
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "75000",
"price": "556699"
}, {
"h_id": "5",
"city": "New York",
"state": "NY",
"zip": "00010",
"price": "962500"
}];
homes.sort(fieldSorter(['price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function(a, b) {
return fields
.map(function(o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o = o.substring(1);
}
if (!parseInt(a[o]) && !parseInt(b[o])) {
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
} else {
return dir > 0 ? a[o] - b[o] : b[o] - a[o];
}
})
.reduce(function firstNonZeroValue(p, n) {
return p ? p : n;
}, 0);
};
}
document.getElementById("output").innerHTML = '<pre>' + JSON.stringify(homes, null, '\t') + '</pre>';
<div id="output">
</div>
A fiddle to test with
Wow, there are some complex solutions here. So complex I decided to come up with something simpler but also quite powerful. Here it is;
function sortByPriority(data, priorities) {
if (priorities.length == 0) {
return data;
}
const nextPriority = priorities[0];
const remainingPriorities = priorities.slice(1);
const matched = data.filter(item => item.hasOwnProperty(nextPriority));
const remainingData = data.filter(item => !item.hasOwnProperty(nextPriority));
return sortByPriority(matched, remainingPriorities)
.sort((a, b) => (a[nextPriority] > b[nextPriority]) ? 1 : -1)
.concat(sortByPriority(remainingData, remainingPriorities));
}
And here is an example of how you use it.
const data = [
{ id: 1, mediumPriority: 'bbb', lowestPriority: 'ggg' },
{ id: 2, highestPriority: 'bbb', mediumPriority: 'ccc', lowestPriority: 'ggg' },
{ id: 3, mediumPriority: 'aaa', lowestPriority: 'ggg' },
];
const priorities = [
'highestPriority',
'mediumPriority',
'lowestPriority'
];
const sorted = sortByPriority(data, priorities);
This will first sort by the precedence of the attributes, then by the value of the attributes.
I think this may be the easiest way to do it.
https://coderwall.com/p/ebqhca/javascript-sort-by-two-fields
It's really simple and I tried it with 3 different key value pairs and it worked great.
Here is a simple example, look at the link for more details
testSort(data) {
return data.sort(
a['nameOne'] > b['nameOne'] ? 1
: b['nameOne'] > a['nameOne'] ? -1 : 0 ||
a['date'] > b['date'] ||
a['number'] - b['number']
);
}

How to sort an array of objects by multiple fields?

From this original question, how would I apply a sort on multiple fields?
Using this slightly adapted structure, how would I sort city (ascending) & then price (descending)?
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
I liked the fact than an answer was given which provided a general approach. Where I plan to use this code, I will have to sort dates as well as other things. The ability to "prime" the object seemed handy, if not a little cumbersome.
I've tried to build this answer into a nice generic example, but I'm not having much luck.
You could use a chained sorting approach by taking the delta of values until it reaches a value not equal to zero.
var data = [{ h_id: "3", city: "Dallas", state: "TX", zip: "75201", price: "162500" }, { h_id: "4", city: "Bevery Hills", state: "CA", zip: "90210", price: "319250" }, { h_id: "6", city: "Dallas", state: "TX", zip: "75000", price: "556699" }, { h_id: "5", city: "New York", state: "NY", zip: "00010", price: "962500" }];
data.sort(function (a, b) {
return a.city.localeCompare(b.city) || b.price - a.price;
});
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Or, using es6, simply:
data.sort((a, b) => a.city.localeCompare(b.city) || b.price - a.price);
for a non-generic, simple solution to your exact problem:
homes.sort(
function(a, b) {
if (a.city === b.city) {
// Price is only important when cities are the same
return b.price - a.price;
}
return a.city > b.city ? 1 : -1;
});
A multi dimensional sorting method, based on this answer:
Update: Here is an "optimized" version. It does a lot more preprocessing and creates a comparison function for each sorting option beforehand. It might need more more memory (as it stores a function for each sorting option, but it should preform a bit better as it does not have to determine the correct settings during the comparison. I have not done any profiling though.
var sort_by;
(function() {
// utility functions
var default_cmp = function(a, b) {
if (a == b) return 0;
return a < b ? -1 : 1;
},
getCmpFunc = function(primer, reverse) {
var dfc = default_cmp, // closer in scope
cmp = default_cmp;
if (primer) {
cmp = function(a, b) {
return dfc(primer(a), primer(b));
};
}
if (reverse) {
return function(a, b) {
return -1 * cmp(a, b);
};
}
return cmp;
};
// actual implementation
sort_by = function() {
var fields = [],
n_fields = arguments.length,
field, name, reverse, cmp;
// preprocess sorting options
for (var i = 0; i < n_fields; i++) {
field = arguments[i];
if (typeof field === 'string') {
name = field;
cmp = default_cmp;
}
else {
name = field.name;
cmp = getCmpFunc(field.primer, field.reverse);
}
fields.push({
name: name,
cmp: cmp
});
}
// final comparison function
return function(A, B) {
var a, b, name, result;
for (var i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
name = field.name;
result = field.cmp(A[name], B[name]);
if (result !== 0) break;
}
return result;
}
}
}());
Example usage:
homes.sort(sort_by('city', {name:'price', primer: parseInt, reverse: true}));
DEMO
Original function:
var sort_by = function() {
var fields = [].slice.call(arguments),
n_fields = fields.length;
return function(A,B) {
var a, b, field, key, primer, reverse, result, i;
for(i = 0; i < n_fields; i++) {
result = 0;
field = fields[i];
key = typeof field === 'string' ? field : field.name;
a = A[key];
b = B[key];
if (typeof field.primer !== 'undefined'){
a = field.primer(a);
b = field.primer(b);
}
reverse = (field.reverse) ? -1 : 1;
if (a<b) result = reverse * -1;
if (a>b) result = reverse * 1;
if(result !== 0) break;
}
return result;
}
};
DEMO
Here is a simple functional generic approach. Specify sort order using array. Prepend minus to specify descending order.
var homes = [
{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":"319250"},
{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":"556699"},
{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":"962500"}
];
homes.sort(fieldSorter(['city', '-price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function (a, b) {
return fields
.map(function (o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o=o.substring(1);
}
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
})
.reduce(function firstNonZeroValue (p,n) {
return p ? p : n;
}, 0);
};
}
Edit: in ES6 it's even shorter!
"use strict";
const fieldSorter = (fields) => (a, b) => fields.map(o => {
let dir = 1;
if (o[0] === '-') { dir = -1; o=o.substring(1); }
return a[o] > b[o] ? dir : a[o] < b[o] ? -(dir) : 0;
}).reduce((p, n) => p ? p : n, 0);
const homes = [{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":162500}, {"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":319250},{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":556699},{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":962500}];
const sortedHomes = homes.sort(fieldSorter(['state', '-price']));
document.write('<pre>' + JSON.stringify(sortedHomes, null, '\t') + '</pre>')
I made a quite generic multi feature sorter today. You can have a look at thenBy.js here: https://github.com/Teun/thenBy.js
It allows you to use the standard Array.sort, but with firstBy().thenBy().thenBy() style. It is way less code and complexity than the solutions posted above.
Here's an extensible way to sort by multiple fields.
homes.sort(function(left, right) {
var city_order = left.city.localeCompare(right.city);
var price_order = parseInt(left.price) - parseInt(right.price);
return city_order || -price_order;
});
Notes
A function passed to array sort is expected to return negative, zero, positive to indicate less, equal, greater.
a.localeCompare(b) is universally supported for strings, and returns -1,0,1 if a<b,a==b,a>b.
Subtraction works on numeric fields, because a - b gives -,0,+ if a<b,a==b,a>b.
|| in the last line gives city priority over price.
Negate to reverse order in any field, as in -price_order
Add new fields to the or-chain: return city_order || -price_order || date_order;
Date compare with subtraction, because date math converts to milliseconds since 1970.var date_order = new Date(left.date) - new Date(right.date);
Boolean compare with subtraction, which is guaranteed to turn true and false to 1 and 0 (therefore the subtraction produces -1 or 0 or 1). var goodness_order = Boolean(left.is_good) - Boolean(right.is_good)This is unusual enough that I'd suggest drawing attention with the Boolean constructor, even if they're already boolean.
This is a complete cheat but I think that it adds value to this question because it's basically a canned library function that you can use out-of-the box.
If your code has access to lodash or a lodash compatible library like underscore then you can use the _.sortBy method. The snippet below is copied directly from the lodash documentation.
The commented results in the examples looks like they return arrays of arrays but that's just showing the order and not the actual results which are an array of objects.
var users = [
{ 'user': 'fred',   'age': 48 },
{ 'user': 'barney', 'age': 36 },
{ 'user': 'fred',   'age': 40 },
{ 'user': 'barney', 'age': 34 }
];
_.sortBy(users, [function(o) { return o.user; }]);
// => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]
_.sortBy(users, ['user', 'age']);
// => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]]
The following function will allow you to sort an array of objects on one or multiple properties, either ascending (default) or descending on each property, and allow you to choose whether or not to perform case sensitive comparisons. By default, this function performs case insensitive sorts.
The first argument must be the array containing the objects.
The subsequent argument(s) must be a comma separated list of strings that reference the different object properties to sort by. The last argument (which is optional) is a boolean to choose whether or not to perform case sensitive sorts - use true for case sensitive sorts.
The function will sort each property/key in ascending order by default. If you want a particular key to sort in descending order, then instead pass in an array in this format: ['property_name', true].
Here are some sample uses of the function followed by an explanation (where homes is an array containing the objects):
objSort(homes, 'city') --> sort by city (ascending, case in-sensitive)
objSort(homes, ['city', true]) --> sort by city (descending, case in-sensitive)
objSort(homes, 'city', true) --> sort by city then price (ascending, case sensitive)
objSort(homes, 'city', 'price') --> sort by city then price (both ascending, case in-sensitive)
objSort(homes, 'city', ['price', true]) --> sort by city (ascending) then price (descending), case in-sensitive)
And without further ado, here's the function:
function objSort() {
var args = arguments,
array = args[0],
case_sensitive, keys_length, key, desc, a, b, i;
if (typeof arguments[arguments.length - 1] === 'boolean') {
case_sensitive = arguments[arguments.length - 1];
keys_length = arguments.length - 1;
} else {
case_sensitive = false;
keys_length = arguments.length;
}
return array.sort(function (obj1, obj2) {
for (i = 1; i < keys_length; i++) {
key = args[i];
if (typeof key !== 'string') {
desc = key[1];
key = key[0];
a = obj1[args[i][0]];
b = obj2[args[i][0]];
} else {
desc = false;
a = obj1[args[i]];
b = obj2[args[i]];
}
if (case_sensitive === false && typeof a === 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (! desc) {
if (a < b) return -1;
if (a > b) return 1;
} else {
if (a > b) return -1;
if (a < b) return 1;
}
}
return 0;
});
} //end of objSort() function
And here's some sample data:
var homes = [{
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": 162500
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": 1000000
}, {
"h_id": "5",
"city": "new york",
"state": "NY",
"zip": "00010",
"price": 1000000
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "85000",
"price": 300000
}, {
"h_id": "7",
"city": "New York",
"state": "NY",
"zip": "00020",
"price": 345000
}];
A dynamic way to do that with MULTIPLE keys:
filter unique values from each col/key of sort
put in order or reverse it
add weights width zeropad for each object based on indexOf(value) keys values
sort using caclutated weights
Object.defineProperty(Array.prototype, 'orderBy', {
value: function(sorts) {
sorts.map(sort => {
sort.uniques = Array.from(
new Set(this.map(obj => obj[sort.key]))
);
sort.uniques = sort.uniques.sort((a, b) => {
if (typeof a == 'string') {
return sort.inverse ? b.localeCompare(a) : a.localeCompare(b);
}
else if (typeof a == 'number') {
return sort.inverse ? b - a : a - b;
}
else if (typeof a == 'boolean') {
let x = sort.inverse ? (a === b) ? 0 : a? -1 : 1 : (a === b) ? 0 : a? 1 : -1;
return x;
}
return 0;
});
});
const weightOfObject = (obj) => {
let weight = "";
sorts.map(sort => {
let zeropad = `${sort.uniques.length}`.length;
weight += sort.uniques.indexOf(obj[sort.key]).toString().padStart(zeropad, '0');
});
//obj.weight = weight; // if you need to see weights
return weight;
}
this.sort((a, b) => {
return weightOfObject(a).localeCompare( weightOfObject(b) );
});
return this;
}
});
Use:
// works with string, number and boolean
let sortered = your_array.orderBy([
{key: "type", inverse: false},
{key: "title", inverse: false},
{key: "spot", inverse: false},
{key: "internal", inverse: true}
]);
Here's a generic multidimensional sort, allowing for reversing and/or mapping on each level.
Written in Typescript. For Javascript, check out this JSFiddle
The Code
type itemMap = (n: any) => any;
interface SortConfig<T> {
key: keyof T;
reverse?: boolean;
map?: itemMap;
}
export function byObjectValues<T extends object>(keys: ((keyof T) | SortConfig<T>)[]): (a: T, b: T) => 0 | 1 | -1 {
return function(a: T, b: T) {
const firstKey: keyof T | SortConfig<T> = keys[0];
const isSimple = typeof firstKey === 'string';
const key: keyof T = isSimple ? (firstKey as keyof T) : (firstKey as SortConfig<T>).key;
const reverse: boolean = isSimple ? false : !!(firstKey as SortConfig<T>).reverse;
const map: itemMap | null = isSimple ? null : (firstKey as SortConfig<T>).map || null;
const valA = map ? map(a[key]) : a[key];
const valB = map ? map(b[key]) : b[key];
if (valA === valB) {
if (keys.length === 1) {
return 0;
}
return byObjectValues<T>(keys.slice(1))(a, b);
}
if (reverse) {
return valA > valB ? -1 : 1;
}
return valA > valB ? 1 : -1;
};
}
Usage Examples
Sorting a people array by last name, then first name:
interface Person {
firstName: string;
lastName: string;
}
people.sort(byObjectValues<Person>(['lastName','firstName']));
Sort language codes by their name, not their language code (see map), then by descending version (see reverse).
interface Language {
code: string;
version: number;
}
// languageCodeToName(code) is defined elsewhere in code
languageCodes.sort(byObjectValues<Language>([
{
key: 'code',
map(code:string) => languageCodeToName(code),
},
{
key: 'version',
reverse: true,
}
]));
Here's another one that's perhaps closer to your idea for the syntax
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {}; // primers are optional
properties = properties.map(function(prop) {
if( !(prop instanceof Array) ) {
prop = [prop, 'asc']
}
if( prop[1].toLowerCase() == 'desc' ) {
prop[1] = -1;
} else {
prop[1] = 1;
}
return prop;
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}
// just for fun use this to reverse the city name when sorting
function demoPrimer(str) {
return str.split('').reverse().join('');
}
// Example
sortObjects(homes, ['city', ['price', 'desc']], {city: demoPrimer});
Demo: http://jsfiddle.net/Nq4dk/2/
Edit: Just for fun, here's a variation that just takes an sql-like string, so you can do sortObjects(homes, "city, price desc")
function sortObjects(objArray, properties /*, primers*/) {
var primers = arguments[2] || {};
properties = properties.split(/\s*,\s*/).map(function(prop) {
prop = prop.match(/^([^\s]+)(\s*desc)?/i);
if( prop[2] && prop[2].toLowerCase() === 'desc' ) {
return [prop[1] , -1];
} else {
return [prop[1] , 1];
}
});
function valueCmp(x, y) {
return x > y ? 1 : x < y ? -1 : 0;
}
function arrayCmp(a, b) {
var arr1 = [], arr2 = [];
properties.forEach(function(prop) {
var aValue = a[prop[0]],
bValue = b[prop[0]];
if( typeof primers[prop[0]] != 'undefined' ) {
aValue = primers[prop[0]](aValue);
bValue = primers[prop[0]](bValue);
}
arr1.push( prop[1] * valueCmp(aValue, bValue) );
arr2.push( prop[1] * valueCmp(bValue, aValue) );
});
return arr1 < arr2 ? -1 : 1;
}
objArray.sort(function(a, b) {
return arrayCmp(a, b);
});
}
Simpler one:
var someArray = [...];
function generateSortFn(props) {
return function (a, b) {
for (var i = 0; i < props.length; i++) {
var prop = props[i];
var name = prop.name;
var reverse = prop.reverse;
if (a[name] < b[name])
return reverse ? 1 : -1;
if (a[name] > b[name])
return reverse ? -1 : 1;
}
return 0;
};
};
someArray.sort(generateSortFn([{name: 'prop1', reverse: true}, {name: 'prop2'}]));
why complicate? just sort it twice! this works perfectly:
(just make sure to reverse the importance order from least to most):
jj.sort( (a, b) => (a.id >= b.id) ? 1 : -1 );
jj.sort( (a, b) => (a.status >= b.status) ? 1 : -1 );
I like SnowBurnt's approach but it needs a tweak to test for equivalence on city NOT a difference.
homes.sort(
function(a,b){
if (a.city==b.city){
return (b.price-a.price);
} else {
return (a.city-b.city);
}
});
Here's my solution based on the Schwartzian transform idiom, hope you find it useful.
function sortByAttribute(array, ...attrs) {
// generate an array of predicate-objects contains
// property getter, and descending indicator
let predicates = attrs.map(pred => {
let descending = pred.charAt(0) === '-' ? -1 : 1;
pred = pred.replace(/^-/, '');
return {
getter: o => o[pred],
descend: descending
};
});
// schwartzian transform idiom implementation. aka: "decorate-sort-undecorate"
return array.map(item => {
return {
src: item,
compareValues: predicates.map(predicate => predicate.getter(item))
};
})
.sort((o1, o2) => {
let i = -1, result = 0;
while (++i < predicates.length) {
if (o1.compareValues[i] < o2.compareValues[i]) result = -1;
if (o1.compareValues[i] > o2.compareValues[i]) result = 1;
if (result *= predicates[i].descend) break;
}
return result;
})
.map(item => item.src);
}
Here's an example how to use it:
let games = [
{ name: 'Pako', rating: 4.21 },
{ name: 'Hill Climb Racing', rating: 3.88 },
{ name: 'Angry Birds Space', rating: 3.88 },
{ name: 'Badland', rating: 4.33 }
];
// sort by one attribute
console.log(sortByAttribute(games, 'name'));
// sort by mupltiple attributes
console.log(sortByAttribute(games, '-rating', 'name'));
Just another option. Consider to use the following utility function:
/** Performs comparing of two items by specified properties
* #param {Array} props for sorting ['name'], ['value', 'city'], ['-date']
* to set descending order on object property just add '-' at the begining of property
*/
export const compareBy = (...props) => (a, b) => {
for (let i = 0; i < props.length; i++) {
const ascValue = props[i].startsWith('-') ? -1 : 1;
const prop = props[i].startsWith('-') ? props[i].substr(1) : props[i];
if (a[prop] !== b[prop]) {
return a[prop] > b[prop] ? ascValue : -ascValue;
}
}
return 0;
};
Example of usage (in your case):
homes.sort(compareBy('city', '-price'));
It should be noted that this function can be even more generalized in order to be able to use nested properties like 'address.city' or 'style.size.width' etc.
Another way
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sortBy(ar) {
return ar.sort((a, b) => a.city === b.city ?
b.price.toString().localeCompare(a.price) :
a.city.toString().localeCompare(b.city));
}
console.log(sortBy(homes));
simply follow the list of your sorting criteria
this code will always remain readable and understandable even if you have 36 sorting criteria to encase
The solution proposed here by Nina is certainly very elegant, but it implies knowing that a value of zero corresponds to a value of false in Boolean logic, and that Boolean tests can return something other than true / false in JavaScript (here are numeric values) which will always be confusing for a beginner.
Also think about who will need to maintain your code. Maybe it would be you: imagine yourself spending your days raking for days the code of another and having a pernicious bug ... and you are exhausted from reading these thousands of lines full of tips
const homes =
[ { h_id: '3', city: 'Dallas', state: 'TX', zip: '75201', price: '162500' }
, { h_id: '4', city: 'Bevery Hills', state: 'CA', zip: '90210', price: '319250' }
, { h_id: '6', city: 'Dallas', state: 'TX', zip: '75000', price: '556699' }
, { h_id: '5', city: 'New York', state: 'NY', zip: '00010', price: '962500' }
]
const fSort = (a,b) =>
{
let Dx = a.city.localeCompare(b.city) // 1st criteria
if (Dx===0) Dx = Number(b.price) - Number(a.price) // 2nd
// if (Dx===0) Dx = ... // 3rd
// if (Dx===0) Dx = ... // 4th....
return Dx
}
console.log( homes.sort(fSort))
Adding a couple helper functions lets you solved this kind of problem generically and simply. sortByKey takes an array and a function which should return a list of items with which to compare each array entry.
This takes advantage of the fact that javascript does smart comparison of arrays of simple values, with [2] < [2, 0] < [2, 1] < [10, 0].
// Two helpers:
function cmp(a, b) {
if (a > b) {
return 1
} else if (a < b) {
return -1
} else {
return 0
}
}
function sortByKey(arr, key) {
arr.sort((a, b) => cmp(key(a), key(b)))
}
// A demonstration:
let arr = [{a:1, b:2}, {b:3, a:0}, {a:1, b:1}, {a:2, b:2}, {a:2, b:1}, {a:1, b:10}]
sortByKey(arr, item => [item.a, item.b])
console.log(JSON.stringify(arr))
// '[{"b":3,"a":0},{"a":1,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":1},{"a":2,"b":2}]'
sortByKey(arr, item => [item.b, item.a])
console.log(JSON.stringify(arr))
// '[{"a":1,"b":1},{"a":2,"b":1},{"a":1,"b":10},{"a":1,"b":2},{"a":2,"b":2},{"b":3,"a":0}]'
I've lovingly stolen this idea from Python's list.sort function.
To make things simple, use these helper functions.
You can sort by as many fields as you need. For each sort field, specify the property name, and then, optionally, specify -1 as the sort direction to sort descending instead of ascending.
const data = [
{"h_id":"3","city":"Dallas","state":"TX","zip":"75201","price":"162500"},
{"h_id":"4","city":"Bevery Hills","state":"CA","zip":"90210","price":"319250"},
{"h_id":"6","city":"Dallas","state":"TX","zip":"75000","price":"556699"},
{"h_id":"5","city":"New York","state":"NY","zip":"00010","price":"962500"},
{"h_id":"7","city":"New York","state":"NY","zip":"00010","price":"800500"}
]
const sortLexically = (p,d=1)=>(a,b)=>d * a[p].localeCompare(b[p])
const sortNumerically = (p,d=1)=>(a,b)=>d * (a[p]-b[p])
const sortBy = sorts=>(a,b)=>sorts.reduce((r,s)=>r||s(a,b),0)
// sort first by city, then by price descending
data.sort(sortBy([sortLexically('city'), sortNumerically('price', -1)]))
console.log(data)
Here is a generic version of #Snowburnt's solution:
var sortarray = [{field:'city', direction:'asc'}, {field:'price', direction:'desc'}];
array.sort(function(a,b){
for(var i=0; i<sortarray.length; i++){
retval = a[sortarray[i].field] < b[sortarray[i].field] ? -1 : a[sortarray[i].field] > b[sortarray[i].field] ? 1 : 0;
if (sortarray[i].direction == "desc") {
retval = retval * -1;
}
if (retval !== 0) {
return retval;
}
}
}
})
This is based on a sort routine I'm using. I didn't test this specific code so it may have errors but you get the idea. The idea is to sort based on the first field that indicates a difference and then stop and go to the next record. So, if you're sorting by three fields and the first field in the compare is enough to determine the sort order of the two records being sorted then return that sort result and go to the next record.
I tested it (actually with a little more complex sort logic) on 5000 records and it did it in the blink of an eye. If you're actually loading more than 1000 records to the client you should probably be using sever-side sorting and filtering.
This code isn't handling case-sensitivity but I leave it to the reader to handle this trivial modification.
function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
Using :
sort(homes, [{city : 'asc'}, {price: 'desc'}])
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
function sort(data, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return data.sort((a, b) => {
for (let i = 0, size = orderBy.length; i < size; i++) {
const key = Object.keys(orderBy[i])[0],
o = orderBy[i][key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return [];
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
}
});
}
console.log(sort(homes, [{city : 'asc'}, {price: 'desc'}]));
// custom sorting by city
const sortArray = ['Dallas', 'New York', 'Beverly Hills'];
const sortData = (sortBy) =>
data
.sort((a, b) => {
const aIndex = sortBy.indexOf(a.city);
const bIndex = sortBy.indexOf(b.city);
if (aIndex < bIndex) {
return -1;
}
if (aIndex === bIndex) {
// price descending
return b.price- a.price;
}
return 1;
});
sortData(sortArray);
You can use lodash orderBy function lodash
It takes two params array of fields, and array of directions ('asc','desc')
var homes = [
{"h_id":"3",
"city":"Dallas",
"state":"TX",
"zip":"75201",
"price":"162500"},
{"h_id":"4",
"city":"Bevery Hills",
"state":"CA",
"zip":"90210",
"price":"319250"},
{"h_id":"6",
"city":"Dallas",
"state":"TX",
"zip":"75000",
"price":"556699"},
{"h_id":"5",
"city":"New York",
"state":"NY",
"zip":"00010",
"price":"962500"}
];
var sorted =. data._.orderBy(data, ['city', 'price'], ['asc','desc'])
A very intuitive functional solution can be crafted by adding 3 relatively simple helpers. Before we dive in, let's start with the usage:
function usage(homes, { asc, desc, fallback }) {
homes.sort(fallback(
asc(home => home.city),
desc(home => parseInt(home.price, 10)),
));
console.log(homes);
}
var homes = [{
h_id: "3",
city: "Dallas",
state: "TX",
zip: "75201",
price: "162500",
}, {
h_id: "4",
city: "Bevery Hills",
state: "CA",
zip: "90210",
price: "319250",
}, {
h_id: "6",
city: "Dallas",
state: "TX",
zip: "75000",
price: "556699",
}, {
h_id: "5",
city: "New York",
state: "NY",
zip: "00010",
price: "962500",
}];
const SortHelpers = (function () {
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
return { asc, desc, fallback };
})();
usage(homes, SortHelpers);
If you scrolled down the snippet you probably already saw the helpers:
const asc = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
Let me quickly explain what each of these functions does.
asc creates a comparator function. The provided function fn is called for both the comparator arguments a and b. The results of the two function calls are then compared. -1 is returned if resultA < resultB, 1 is returned if resultA > resultB, or 0 otherwise. These return values correspond with an ascending order direction.
It could also be written like this:
function asc(fn) {
return function (a, b) {
// apply `fn` to both `a` and `b`
a = fn(a);
b = fn(b);
if (a < b) return -1;
if (a > b) return 1;
return 0;
// or `return -(a < b) || +(a > b)` for short
};
}
desc is super simple, since it just calls asc but swaps the a and b arguments, resulting in descending order instead of ascending.
fallback (there might be a better name for this) allows us to use multiple comparator functions with a single sort.
Both asc and desc can be passed to sort by themself.
homes.sort(asc(home => home.city))
There is however an issue if you want to combine multiple comparator functions. sort only accepts a single comparator function. fallback combines multiple comparator functions into a single comparator.
The first comparator is called with arguments a and b, if the comparator returns the value 0 (meaning that the values are equal) then we fall back to the next comparator. This continues until a non-0 value is found, or until all comparators are called, in which case the return value is 0.
You can provide your custom comparator functions to fallback() as well. Say you want to use localeCompare() instead of comparing strings with < and >. In such a case you can replace asc(home => home.city) with (a, b) => a.city.localeCompare(b.city).
homes.sort(fallback(
(a, b) => a.city.localeCompare(b.city),
desc(home => parseInt(home.price, 10)),
));
One thing to note is that values that can be undefined will always return false when comparing with < and >. So if a value can be missing you might want to sort by its presence first.
homes.sort(fallback(
// homes with optionalProperty first, true (1) > false (0) so we use desc
desc(home => home.optionalProperty != null), // checks for both null and undefined
asc(home => home.optionalProperty),
// ...
))
Since comparing strings with localeCompare() is such a common thing to do, you could include this as part of asc().
function hasMethod(item, methodName) {
return item != null && typeof item[methodName] === "function";
}
function asc(fn) {
return function (a, b) {
a = fn(a);
b = fn(b);
const areLocaleComparable =
hasMethod(a, "localeCompare") && hasMethod(b, "localeCompare");
if (areLocaleComparable) return a.localeCompare(b);
return -(a < b) || +(a > b);
};
}
Here, you can try the smaller and convenient way to sort by multiple fields!
var homes = [
{ "h_id": "3", "city": "Dallas", "state": "TX", "zip": "75201", "price": "162500" },
{ "h_id": "4", "city": "Bevery Hills", "state": "CA", "zip": "90210", "price": "319250" },
{ "h_id": "6", "city": "Dallas", "state": "TX", "zip": "75000", "price": "556699" },
{ "h_id": "5", "city": "New York", "state": "NY", "zip": "00010", "price": "962500" }
];
homes.sort((a, b)=> {
if (a.city === b.city){
return a.price < b.price ? -1 : 1
} else {
return a.city < b.city ? -1 : 1
}
})
console.log(homes);
function sortMultiFields(prop){
return function(a,b){
for(i=0;i<prop.length;i++)
{
var reg = /^\d+$/;
var x=1;
var field1=prop[i];
if(prop[i].indexOf("-")==0)
{
field1=prop[i].substr(1,prop[i].length);
x=-x;
}
if(reg.test(a[field1]))
{
a[field1]=parseFloat(a[field1]);
b[field1]=parseFloat(b[field1]);
}
if( a[field1] > b[field1])
return x;
else if(a[field1] < b[field1])
return -x;
}
}
}
How to use (put -(minus) sign before field if you want to sort in descending order particular field)
homes.sort(sortMultiFields(["city","-price"]));
Using above function you can sort any json array with multiple fields. No need to change function body at all
Adaptation of #chriskelly 's answer.
Most answers overlook that price will not sort properly if the value is in the ten thousands and lower or over a million. The resaon being JS sorts alphabetically. It was answered pretty well here, Why can't JavaScript sort "5, 10, 1" and here How to sort an array of integers correctly.
Ultimately we have to do some evaluation if the field or node we're sorting by is an number. I am not saying that using parseInt() in this case is the correct answer, the sorted results are more important.
var homes = [{
"h_id": "2",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62500"
}, {
"h_id": "1",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "62510"
}, {
"h_id": "3",
"city": "Dallas",
"state": "TX",
"zip": "75201",
"price": "162500"
}, {
"h_id": "4",
"city": "Bevery Hills",
"state": "CA",
"zip": "90210",
"price": "319250"
}, {
"h_id": "6",
"city": "Dallas",
"state": "TX",
"zip": "75000",
"price": "556699"
}, {
"h_id": "5",
"city": "New York",
"state": "NY",
"zip": "00010",
"price": "962500"
}];
homes.sort(fieldSorter(['price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative
function fieldSorter(fields) {
return function(a, b) {
return fields
.map(function(o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o = o.substring(1);
}
if (!parseInt(a[o]) && !parseInt(b[o])) {
if (a[o] > b[o]) return dir;
if (a[o] < b[o]) return -(dir);
return 0;
} else {
return dir > 0 ? a[o] - b[o] : b[o] - a[o];
}
})
.reduce(function firstNonZeroValue(p, n) {
return p ? p : n;
}, 0);
};
}
document.getElementById("output").innerHTML = '<pre>' + JSON.stringify(homes, null, '\t') + '</pre>';
<div id="output">
</div>
A fiddle to test with
Wow, there are some complex solutions here. So complex I decided to come up with something simpler but also quite powerful. Here it is;
function sortByPriority(data, priorities) {
if (priorities.length == 0) {
return data;
}
const nextPriority = priorities[0];
const remainingPriorities = priorities.slice(1);
const matched = data.filter(item => item.hasOwnProperty(nextPriority));
const remainingData = data.filter(item => !item.hasOwnProperty(nextPriority));
return sortByPriority(matched, remainingPriorities)
.sort((a, b) => (a[nextPriority] > b[nextPriority]) ? 1 : -1)
.concat(sortByPriority(remainingData, remainingPriorities));
}
And here is an example of how you use it.
const data = [
{ id: 1, mediumPriority: 'bbb', lowestPriority: 'ggg' },
{ id: 2, highestPriority: 'bbb', mediumPriority: 'ccc', lowestPriority: 'ggg' },
{ id: 3, mediumPriority: 'aaa', lowestPriority: 'ggg' },
];
const priorities = [
'highestPriority',
'mediumPriority',
'lowestPriority'
];
const sorted = sortByPriority(data, priorities);
This will first sort by the precedence of the attributes, then by the value of the attributes.
I think this may be the easiest way to do it.
https://coderwall.com/p/ebqhca/javascript-sort-by-two-fields
It's really simple and I tried it with 3 different key value pairs and it worked great.
Here is a simple example, look at the link for more details
testSort(data) {
return data.sort(
a['nameOne'] > b['nameOne'] ? 1
: b['nameOne'] > a['nameOne'] ? -1 : 0 ||
a['date'] > b['date'] ||
a['number'] - b['number']
);
}

Categories