Reputation: 5998
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.
Upvotes: 406
Views: 487634
Reputation: 411
easy and understandable:
var homes = [
{ 'city': 'Dallas', 'state': 'TX', 'zip': '75201', 'price': '162500'},
{ 'city`enter code here`': 'Bevery Hills', 'state': 'CA', 'zip': '90210', 'price': '319250'},
{ 'city': 'Dallas', 'state': 'TX', 'zip': '75000', 'price': '556699'},
{ 'city': 'New York', 'state': 'NY', 'zip': '00010', 'price': '962500'}
];
homes.sort(compareMultiple(['zip', '-state', 'price']));
function compareMultiple (criteria) {
return function (a, b) {
for (let key of criteria) {
var order = key.includes('-') ? -1 : 1;
if (!a[key]) return -order;
if (!b[key]) return order;
if (!a[key] && ![key]) return 0;
if (a[key] > b[key]) return order;
if (a[key] < b[key]) return -order;
}
return 0;
};
}
Upvotes: 1
Reputation: 1300
Fastest and easiest way is to use OR-chaining as many of people already suggested here. For the specified example data it looks like this:
homes.sort((a, b) =>
a.city.localeCompare(b.city)
|| (Number(b.price) - Number(a.price))
);
But if you want something configurable (and in TypeScript), you can try the following code:
export type Comparer<T> = (a: T, b: T) => number;
export type CompareCriterion<TItem, TValue> = {
selector: (item: TItem) => TValue,
descending?: boolean,
comparer?: Comparer<TValue>,
};
export const defaultComparer = <T>(a: T, b: T): number => {
return a === b ? 0 : a > b ? 1 : -1;
};
export const defaultNumberComparer = (a: number, b: number): number => {
return a - b;
};
export const StringComparer = (() => {
const currentLocale = new Intl.Collator(navigator.language, { usage: 'sort', sensitivity: 'variant', caseFirst: 'upper' });
const currentLocaleIgnoreCase = new Intl.Collator(navigator.language, { usage: 'sort', sensitivity: 'accent', caseFirst: 'upper' });
const invariantLocale = new Intl.Collator('en', { usage: 'sort', sensitivity: 'variant', caseFirst: 'upper' });
const invariantLocaleIgnoreCase = new Intl.Collator('en', { usage: 'sort', sensitivity: 'accent', caseFirst: 'upper' });
return {
// eslint-disable-next-line @typescript-eslint/unbound-method
currentLocale: currentLocale.compare,
// eslint-disable-next-line @typescript-eslint/unbound-method
currentLocaleIgnoreCase: currentLocaleIgnoreCase.compare,
// eslint-disable-next-line @typescript-eslint/unbound-method
invariantLocale: invariantLocale.compare,
// eslint-disable-next-line @typescript-eslint/unbound-method
invariantLocaleIgnoreCase: invariantLocaleIgnoreCase.compare,
};
})();
export const defaultStringComparer = (a: string, b: string): number => {
return a.localeCompare(b);
};
export const defaultDateComparer = (a: Date, b: Date): number => {
return a.getTime() - b.getTime();
};
export class ComparerBuilder<TItem> {
#criteria: ((next?: Comparer<TItem>) => Comparer<TItem>)[] = [];
add<TValue>(criterion: CompareCriterion<TItem, TValue>): ComparerBuilder<TItem> {
this.#criteria.push(next => ComparerBuilder.#createComparer(criterion, next));
return this;
}
static #createComparer<TItem, TValue>(
criterion: CompareCriterion<TItem, TValue>,
next?: Comparer<TItem>,
): Comparer<TItem> {
const comparer = criterion.comparer ?? defaultComparer;
return (a: TItem, b: TItem) => {
const av = criterion.selector(a);
const bv = criterion.selector(b);
const comparison = comparer(av, bv);
if (comparison === 0)
return next?.(a, b) ?? 0;
return criterion.descending ? -comparison : comparison;
};
}
build(bottomComparer?: Comparer<TItem>): Comparer<TItem> {
let comparer = bottomComparer;
for (let i = this.#criteria.length - 1; i >= 0; i--)
comparer = this.#criteria[i](comparer);
return comparer ?? defaultComparer;
}
}
// Declare item type.
type Item = { key: number, code: string, name: string, price: number };
// Build comparer from provided criteria.
const comparer = new ComparerBuilder<Item>()
.add({ selector: v => v.price })
.add({ selector: v => v.code, descending: true, comparer: StringComparer.currentLocaleIgnoreCase })
.add({ selector: v => v.name, comparer: new Intl.Collator('ru').compare })
.add({ selector: v => v.key, comparer: defaultNumberComparer })
.build();
// Use built comparer for multiple calls.
const items1: Item[] = [{ key: 1, code: 'FOO', name: 'bar', price: 100.98 }, { key: 2, code: 'FOa', name: 'baz', price: 100.98 }];
// Note: we are using spread operator to prevent original array mutation (sort method works so).
const sortedItems1 = [...items1].sort(comparer);
const items2: Item[] = [{ key: 1, code: 'BAR', name: 'foo', price: 100.98 }];
// Note: we are using spread operator to prevent original array mutation (sort method works so).
const sortedItems2 = [...items2].sort(comparer);
Upvotes: 1
Reputation: 885
// Array of objects representing the data
const data = [
{ name: 'John', surname: 'Doe', birthdate: new Date(1980, 5, 15) },
{ name: 'Jane', surname: 'Smith', birthdate: new Date(1990, 2, 28) },
{ name: 'Alex', surname: 'Johnson', birthdate: new Date(1985, 8, 10) },
// Additional objects...
];
// Custom comparator function for multiple field sorting
function multiFieldSort(a, b) {
// Sorting fields and orders
const fields = [
{ name: 'name', order: 'asc' },
{ name: 'surname', order: 'desc' },
{ name: 'birthdate', order: 'desc' },
];
// Iterate over fields and perform comparisons
for (const field of fields) {
const aValue = a[field.name];
const bValue = b[field.name];
let comparison = 0;
if (typeof aValue === 'string' && typeof bValue === 'string') {
comparison = aValue.localeCompare(bValue);
} else if (typeof aValue === 'number' && typeof bValue === 'number') {
comparison = aValue - bValue;
} else if (aValue instanceof Date && bValue instanceof Date) {
comparison = aValue.getTime() - bValue.getTime();
}
if (comparison !== 0) {
return field.order === 'asc' ? comparison : -comparison;
}
}
// Default case: preserve the original order
return 0;
}
// Sort the data array using the multiFieldSort function
data.sort(multiFieldSort);
// Output the sorted data
console.log(data);
Upvotes: 0
Reputation: 17204
To sort an array of objects 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.localeCompare(b)
is universally supported for strings, and returns -1,0,1 if a<b
,a==b
,a>b
.a - b
gives -,0,+ if a<b
,a==b
,a>b
.||
in the last line gives city
priority over price
.-price_order
return city_order || -price_order || date_order;
var date_order = new Date(left.date) - new Date(right.date);
CAUTION: Date() returns a string, and is freakishly different from the new Date() constructor.var goodness_order = Boolean(left.is_good) - Boolean(right.is_good)
Upvotes: 29
Reputation: 955
my humble proposal:
function cmp(a, b) {
if (a > b) return 1;
if (a < b) return -1;
return 0;
}
function objCmp(a, b, fields) {
for (let field of fields) {
let ret =
cmp(a[field], b[field]);
if (ret != 0) {
return ret;
}
}
return 0;
}
with these two functions, you may sort quite elegantly an array of objects the following way:
let sortedArray = homes.sort(
(a, b) => objCmp(
a, b, ['state', 'city'])
);
Upvotes: 2
Reputation: 8087
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)
Upvotes: 3
Reputation: 2957
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);
Upvotes: 1
Reputation: 21110
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);
};
}
Upvotes: 3
Reputation: 38462
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.
Upvotes: 2
Reputation: 6922
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;
});
Upvotes: 356
Reputation: 137
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 );
Upvotes: 7
Reputation: 331
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'])
Upvotes: 1
Reputation: 7736
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>')
Upvotes: 95
Reputation: 22265
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))
Upvotes: 2
Reputation: 597
// 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);
Upvotes: 1
Reputation: 1762
A dynamic way to do that with MULTIPLE keys:
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}
]);
Upvotes: 11
Reputation: 604
Simplest Way to sort array of object by multiple fields:
let 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) => (a.city > b.city) ? 1 : -1);
Output: "Bevery Hills" "Dallas" "Dallas" "Dallas" "New York"
Upvotes: -6
Reputation: 386520
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);
Upvotes: 465
Reputation: 515
This is a recursive algorithm to sort by multiple fields while having the chance to format values before comparison.
var data = [
{
"id": 1,
"ship": null,
"product": "Orange",
"quantity": 7,
"price": 92.08,
"discount": 0
},
{
"id": 2,
"ship": "2017-06-14T23:00:00.000Z".toDate(),
"product": "Apple",
"quantity": 22,
"price": 184.16,
"discount": 0
},
...
]
var sorts = ["product", "quantity", "ship"]
// comp_val formats values and protects against comparing nulls/undefines
// type() just returns the variable constructor
// String.lower just converts the string to lowercase.
// String.toDate custom fn to convert strings to Date
function comp_val(value){
if (value==null || value==undefined) return null
var cls = type(value)
switch (cls){
case String:
return value.lower()
}
return value
}
function compare(a, b, i){
i = i || 0
var prop = sorts[i]
var va = comp_val(a[prop])
var vb = comp_val(b[prop])
// handle what to do when both or any values are null
if (va == null || vb == null) return true
if ((i < sorts.length-1) && (va == vb)) {
return compare(a, b, i+1)
}
return va > vb
}
var d = data.sort(compare);
console.log(d);
If a and b are equal it will just try the next field until none is available.
Upvotes: 0
Reputation: 434
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.
Upvotes: 3
Reputation: 495
Here's a generic multidimensional sort, allowing for reversing and/or mapping on each level.
Written in Typescript. For Javascript, check out this JSFiddle
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;
};
}
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,
}
]));
Upvotes: 8
Reputation: 155
I was looking for something similar and ended up with this:
First we have one or more sorting functions, always returning either 0, 1 or -1:
const sortByTitle = (a, b): number =>
a.title === b.title ? 0 : a.title > b.title ? 1 : -1;
You can create more functions for each other property you want to sort on.
Then I have a function that combines these sorting functions into one:
const createSorter = (...sorters) => (a, b) =>
sorters.reduce(
(d, fn) => (d === 0 ? fn(a, b) : d),
0
);
This can be used to combine the above sorting functions in a readable way:
const sorter = createSorter(sortByTitle, sortByYear)
items.sort(sorter)
When a sorting function returns 0 the next sorting function will be called for further sorting.
Upvotes: 0
Reputation: 397
Here is mine for your reference, with example:
function msort(arr, ...compFns) {
let fn = compFns[0];
arr = [].concat(arr);
let arr1 = [];
while (arr.length > 0) {
let arr2 = arr.splice(0, 1);
for (let i = arr.length; i > 0;) {
if (fn(arr2[0], arr[--i]) === 0) {
arr2 = arr2.concat(arr.splice(i, 1));
}
}
arr1.push(arr2);
}
arr1.sort(function (a, b) {
return fn(a[0], b[0]);
});
compFns = compFns.slice(1);
let res = [];
arr1.map(a1 => {
if (compFns.length > 0) a1 = msort(a1, ...compFns);
a1.map(a2 => res.push(a2));
});
return res;
}
let tstArr = [{ id: 1, sex: 'o' }, { id: 2, sex: 'm' }, { id: 3, sex: 'm' }, { id: 4, sex: 'f' }, { id: 5, sex: 'm' }, { id: 6, sex: 'o' }, { id: 7, sex: 'f' }];
function tstFn1(a, b) {
if (a.sex > b.sex) return 1;
else if (a.sex < b.sex) return -1;
return 0;
}
function tstFn2(a, b) {
if (a.id > b.id) return -1;
else if (a.id < b.id) return 1;
return 0;
}
console.log(JSON.stringify(msort(tstArr, tstFn1, tstFn2)));
//output:
//[{"id":7,"sex":"f"},{"id":4,"sex":"f"},{"id":5,"sex":"m"},{"id":3,"sex":"m"},{"id":2,"sex":"m"},{"id":6,"sex":"o"},{"id":1,"sex":"o"}]
Upvotes: 0
Reputation: 125
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']
);
}
Upvotes: 0
Reputation: 1
How about this simple solution:
const sortCompareByCityPrice = (a, b) => {
let comparison = 0
// sort by first criteria
if (a.city > b.city) {
comparison = 1
}
else if (a.city < b.city) {
comparison = -1
}
// If still 0 then sort by second criteria descending
if (comparison === 0) {
if (parseInt(a.price) > parseInt(b.price)) {
comparison = -1
}
else if (parseInt(a.price) < parseInt(b.price)) {
comparison = 1
}
}
return comparison
}
Based on this question javascript sort array by multiple (number) fields
Upvotes: -1
Reputation: 2942
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.
Upvotes: 0
Reputation: 842
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>
Upvotes: 0
Reputation: 26784
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));
Upvotes: 2
Reputation: 237
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'}]));
Upvotes: 1
Reputation: 9474
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'));
Upvotes: 3