Sort array of objects by multiple properties in Javascript - javascript

I've been reading similar posts all day but can't figure out how to sort my javascript array by multiple properties.
My array has a 'name' and 'type' property.
To sort by name I now use:
byNameDesc.sort(function (a, b) {
var x = a.name.toLowerCase();
var y = b.name.toLowerCase();
return y < x ? -1 : y > x ? 1 : 0;
});
Works great. I want to enhance this function. If 'name' is 'foo' it should always be on top. And I also want to sort by 'type'.
So 'foo' should always be on top, next sort by 'name' and 'type'.
I tried this:
byNameDefault.sort(function (a, b) {
if (a.name == 'foo') {
return -1;
}
var x = a.type.toLowerCase();
var y = b.type.toLowerCase();
return x < y ? -1 : x > y ? 1 : 0;
});
But that didn't work.
And I have no clue how to sort by 'name' AND 'type'.
Any help is much appreciated.

For multiple sort criteria you proceed from the first to the last criterion:
If the two entries for one criterion are not equal, you can return from the sort function with result -1 or 1. Additionally at the last criterion you also can return 0 for two equal inputs.
Here is an example implementation for your case:
byNameDefault.sort(function (a, b) {
// compare names
var na = a.name.toLowerCase();
var nb = b.name.toLowerCase();
if (na !== nb) {
if (na === 'foo')
return -1;
else if (nb === 'foo')
return 1;
else
return na < nb ? -1 : 1;
}
// compare types
return a.type < b.type ? -1 : a.type > b.type ? 1 : 0;
}

Do this in one expression where the different components are combined with ||; only when one part evaluates to 0, then next one comes into play:
byNameDefault.sort(function (a, b) {
return (b === 'foo') - (a == 'foo') ||
a.name.localeCompare(b.name) ||
a.type.localeCommpare(b.type);
}

Related

Alphanumeric and numeric strings sorting not working as expected

I implemented below logic to apply sorting on mixed data(contains alphanumeric and numeric values) but it is not sorting as expected.
/*For numeric value sorting */
if (!isNaN(fVal) && !isNaN(lastVal)) {
switch (policy) {
case SORT_BY_DESC:
return +fVal < +lastVal ? 1 : -1;
case SORT_BY_ASC:
return +fVal > +lastVal ? 1 : -1;
default:
return 0;
}
}
/* For alphanumeric sorting */
else {
switch (policy) {
case SORT_BY_DESC:
return fVal < lastVal ? 1 : -1;
case SORT_BY_ASC:
return fVal > lastVal ? 1 : -1;
default:
return 0;
}
}
If all the values are numeric this logic is working fine but if I have mixed data it is not sorting properly.
Raw Data - ['60091A0222', '633', '63372A1019', '63372A1021', '6667', '6789', '7776']
Expected Result -
Data in Descending order - 63372A1021,
63372A1019,
60091A0222,
7776,
6789,
633
Data in Ascending order - 633,,
6667,
6789,
7776,
60091A0222,
63372A1019,
63372A1021
What I am getting -
Descending order - 7776,
6789,
6667,
63372A1021,
63372A1019,
633,
60091A0222
Ascending order - 60091A0222,
633,
63372A1019,
63372A1021,
6667,
6789,
7776
You could take a check for finiteness and sort this value to top.
const
data = ['60091A0222', '833', '63372A1019', '63372A1021', '6667', '6789', '7776'],
asc = (a, b) => isFinite(b) - isFinite(a) || (+a > +b) - (+a < +b) || (a > b) - (a < b),
desc = (a, b) => asc(b, a);
data.sort(asc);
console.log(...data);
data.sort(desc);
console.log(...data);
You will need to split the strings into number/letter/number groups and sort based on the presence of each type of group.
const sortIndices = (data, desc = false) => {
const pattern = /(?<=\d)(?=\D)|(?=\d)(?<=\D)/, dir = desc ? -1 : 1;
return data.sort((a, b) => {
const x = a.split(pattern), y = b.split(pattern);
let diff;
// Compare overall length
if (a.length < b.length) return -1 * dir;
if (b.length < a.length) return 1 * dir;
// Find the difference between the first group (numeric)
diff = +x[0] - +y[0];
if (diff !== 0) return diff * dir;
// If both contain at least two groups
if (a.length < 2) return -1 * dir;
if (b.length < 2) return 1 * dir;
// Find the difference between the second group (alpha)
diff = x[1].localeCompare(y[1]);
if (diff !== 0) return diff * dir;
// If both contain at least three groups
if (a.length < 3) return -1 * dir;
if (b.length < 3) return 1 * dir;
// Find the difference between the third group (numeric)
return (+x[2] - +y[2]) * dir;
});
};
const arr = ['60091A0222', '633', '63372A1019', '63372A1021', '6667', '6789', '7776'];
console.log(...sortIndices(arr)); // Ascending
console.log(...sortIndices(arr, true)); // Descending
.as-console-wrapper { top: 0; max-height: 100% !important; }
The main issue with your current code is that the if-statement only accounts for 2 scenarios.
Both values are not NaN.
Both values are NaN.
You have not specified what should happens if one value is a number and the other one is a non-number. They currently fall into the else-logic and are ordered based on ASCII values.
However from you desired output it seems that you first want to order based on the number/non-number. This is then followed by either a value comparison for the number values, or an ASCII code-point comparison for non-number values.
With a minimum amount of changes to your current code, this might look like this:
// one value is a number, the other one is not
if (isNaN(fVal) !== isNaN(lastVal)) {
switch (policy) {
case SORT_BY_DESC:
return isNaN(fVal) ? -1 : 1; // move non-numbers to the front
case SORT_BY_ASC:
return isNaN(fVal) ? 1 : -1; // move non-numbers to the back
default:
return 0;
}
}
// both values are numbers
if (!isNaN(fVal) && !isNaN(lastVal)) { // <- One check is obsolete. We know that
switch (policy) { // either both are a number, or both are
case SORT_BY_DESC: // a non-number, so one check suffices.
return +fVal < +lastVal ? 1 : -1;
case SORT_BY_ASC:
return +fVal > +lastVal ? 1 : -1;
default:
return 0;
}
}
// both values are non-numbers
/* For alphanumeric sorting */
else { // <- Obsolete else, every path in the if returns the function.
switch (policy) {
case SORT_BY_DESC:
return fVal < lastVal ? 1 : -1;
case SORT_BY_ASC:
return fVal > lastVal ? 1 : -1;
default:
return 0;
}
}

Javascript Sorting an Array like order by in Oracle

I have an Array that I need to sort exactly like using order by in Oracle SQl.
If I have following Array:
var array = ['Ba12nes','Apfel','Banane','banane','abc','ABC','123','2', null,'ba998ne']
var.sort(compare);
I would like to have the following result
var array = ['abc','ABC','Apfel','banane','Banane','Ba12nes','ba998ne','123','2', null]
If the null values are somewhere else, I don't have a Problem with it.
My current solution, which does not help me ^^
function compare(a,b) {
if(a == null)
return -1;
if (b == null)
return 1;
if (a.toLowerCase() < b.toLowerCase())
return -1;
if (a.toLowerCase() > b.toLowerCase())
return 1;
return 0;
}
I do understand that i need a custom sorting function. And at the moment I am thinking that only a regular expression can solve the problem of sorting the string values in front of the numbers. But I am still not sure how to solve the Problem with lowercase letters in bevor Uppercase letters.
Iirc, Oracle implements a 3-tiered lexicographic sorting (but heed the advice of Alex Poole and check the NLS settings first):
First sort by base characters ignoring case and diacritics, digits come after letters in the collation sequence.
Second, on ties sort respecting diacritics, ignoring case.
Third, on ties sort by case.
You can emulate the behavior using javascript locale apis by mimicking each step in turn in a custom compare function, with the exception of the letter-digit inversion in the collation sequence.
Tackle the latter by identifying 10 contiguous code points that do not represent digits and that lie beyond the set of code points that may occur in the strings you are sorting. Map digits onto the the chosen code point range preserving order. When you sort, specify the Unicode collating extension 'direct' which means 'sorting by code point'. Remap after sorting.
In the PoC code below I have chosen some cyrillic characters.
function cmptiered(a,b) {
//
// aka oracle sort
//
return lc_base.compare(a, b) || lc_accent.compare(a, b) || lc_case.compare(a, b);
} // cmptiered
var lc_accent = new Intl.Collator('de', { sensitivity: 'accent' });
var lc_base = new Intl.Collator('de-DE-u-co-direct', { sensitivity: 'base' });
var lc_case = new Intl.Collator('de', { caseFirst: 'lower', sensitivity: 'variant' });
var array = ['Ba12nes','Apfel','Banane','banane','abc','ABC','123','2', null, 'ba998ne' ];
// Map onto substitute code blocks
array = array.map ( function ( item ) { return (item === null) ? null : item.replace ( /[0-9]/g, function (c) { return String.fromCharCode(c.charCodeAt(0) - "0".charCodeAt(0) + "\u0430".charCodeAt(0)); } ); } );
array.sort(cmptiered);
// Remap substitute code point
array = array.map ( function ( item ) { return (item === null) ? null : item.replace ( /[\u0430-\u0439]/g, function (c) { return String.fromCharCode(c.charCodeAt(0) - "\u0430".charCodeAt(0) + "0".charCodeAt(0)); } ); } );
Edit
Function cmptiered streamlined following Nina Scholz' comment.
This proposals feature sorting without use of Intl.Collator. The first solution works with direct sort and comparing the given values.
var array = ['Ba12nes', 'Apfel', 'Banane', 'banane', 'abc', 'ABC', '123', '2', null, 'ba998ne'];
array.sort(function (a, b) {
var i = 0;
if (a === null && b === null) { return 0; }
if (a === null) { return 1; }
if (b === null) { return -1; }
while (i < a.length && i < b.length && a[i].toLocaleLowerCase() === b[i].toLocaleLowerCase()) {
i++;
}
if (isFinite(a[i]) && isFinite(b[i])) { return a[i] - b[i]; }
if (isFinite(a[i])) { return 1; }
if (isFinite(b[i])) { return -1; }
return a.localeCompare(b);
});
document.write(JSON.stringify(array));
The second solution features a different approach, based on Sorting with map and a custom sort scheme which takes a new string. The string is build by this rules:
If the value is null take the string 'null'.
If a character is a decimal, takes the character with space paddded around, eg. if it is 9 take the string ' 9 '.
Otherwise for every other character take two spaces and the character itself, like ' o'.
The new build string is used with a a.value.localeCompare(b.value).
Here are the strings with the mapped values:
' B a 1 2 n e s'
' A p f e l'
' B a n a n e'
' b a n a n e'
' a b c'
' A B C'
' 1 2 3 '
' 2 '
'null'
' b a 9 9 8 n e'
sorted, it became
' a b c'
' A B C'
' A p f e l'
' b a n a n e'
' B a n a n e'
' B a 1 2 n e s'
' b a 9 9 8 n e'
' 1 2 3 '
' 2 '
'null'
var array = ['Ba12nes', 'Apfel', 'Banane', 'banane', 'abc', 'ABC', '123', '2', null, 'ba998ne'],
mapped = array.map(function (el, i) {
var j, o = { index: i, value: '' };
if (el === null) {
o.value = 'null';
return o;
}
for (j = 0; j < el.length; j++) {
o.value += /\d/.test(el[j]) ? ' ' + el[j] + ' ' : ' ' + el[j];
}
return o;
});
mapped.sort(function (a, b) {
return a.value.localeCompare(b.value);
});
var result = mapped.map(function (el) {
return array[el.index];
});
document.write(JSON.stringify(result));
A simple head on solution that works at least for english & russian (mimicking NLS_SORT=RUSSIAN) and doesn't rely on fancy things like Intl.Collator, locales and options that don't exist for IE<11.
function compareStringOracle(str1, str2) {
if (str1 == null && str2 != null)
return 1;
else if (str1 != null && str2 == null)
return -1;
else if (str1 == null && str2 == null)
return 0;
else {
return compareStringCaseInsensitiveDigitsLast(str1, str2) ||
/* upper case wins between otherwise equal values, which can be checked with
a simple binary comparison (at least for eng & rus) */
((str1 < str2) ? -1 : (str1 > str2) ? 1 : 0);
}
}
function compareStringCaseInsensitiveDigitsLast(str1, str2) {
for (var i = 0; i < str1.length; ++i) {
if (i === str2.length)
return 1;
// toLocaleLowerCase is unnecessary for eng & rus
var c1 = str1.charAt(i).toLowerCase();
var c2 = str2.charAt(i).toLowerCase();
var d1 = "0" <= c1 && c1 <= "9";
var d2 = "0" <= c2 && c2 <= "9";
if (!d1 && d2)
return -1;
else if (d1 && !d2)
return 1;
else if (c1 !== c2)
return (c1 < c2) ? -1 : (c1 > c2) ? 1 : 0;
}
if (str1.length < str2.length)
return -1;
else
return 0;
}

How can a sorted javascript array return incorrect values from the comparator?

Say I have a list of items, which are sorted using a given comparator. I would expect that after sorting into ascending order comparator(element[1], element[1+n]) should return -1 for all values of n> 1, because according to that comparator, element[1]
I am performing a custom sort and finding that after sorting there are instances where comparator(element[1], element[1+n]) returns 1. When I look at the instance I see that the comparator giving the correct output, i.e. element[1]>element[1+n]. I don't understand how this could be the case after performing a sort with that comparator.
If anyone has any idea of sort subtleties that I might have missed, I'd really appreciate their thoughts. Also, if I can provide more information that might shed light please let me know.
edit
I thought this might be a more general question, but in response to mplungjan have added the custom sorter below.
The sort is for a hierarchical dataset in the form of a flat list of objects. Each object has an id which might be as follows:
0 for root 1.
0-0 for its first child.
0-1 for its second child.
etc.
Each object in the list had a field 'parent' which has the id of its parent. Essentially data.sort isn't doing what I think it should be.
function CurrencyTreeSorter(a, b) {
a_id = a.id.split("-");
b_id = b.id.split("-");
if(a_id.length != b_id.length || a_id.slice(0, a_id.length-1).toString() != b_id.slice(0, b_id.length-1).toString()){
var i = 0;
while (i < a_id.length && i < b_id.length && a_id[i] == b_id[i]) {
i++;
}
if (i == a_id.length || i == b_id.length){
return a_id.length > b_id.length ? 1 : -1;
}
else{
while (a.level > i) {
a = getParent(dataMap, a);
}
while (b.level > i) {
b = getParent(dataMap, b);
}
}
}
var x, y;
if (a[sortcol] == "-") {
x = -1;
}
else {
x = parseFloat(a[sortcol].replace(/[,\$£€]/g, ""));
}
if (b[sortcol] == "-") {
y = -1;
}
else {
y = parseFloat(b[sortcol].replace(/[,\$£€]/g, ""));
}
return sortdir * (x == y ? 0 : (x > y ? 1 : -1));
}
This turned out to be an issue with Chrome, described here and here. Essentially it's not safe to return zero from the comparator/comparer.

How can I add a sort function condition that will sort all the blank entries to the end of the list?

I'm sorting an object array that has a primary contact name, among other things. Sometimes this has a blank value and when I use the function below it sorts it all correctly, but all the blanks go at the top of the list instead of the bottom. I thought that adding the condition shown below would work, but it does not.
this.comparePrimaryContactName = function (a, b)
{
if(a.PrimaryContactName == "") return -1;
return a.PrimaryContactName > b.PrimaryContactName ? 1 : -1;
}
What am I missing?
I usually use something like this:
this.comparePrimaryContactName = function(a, b) {
a = a.PrimaryContactName || '';
b = b.PrimaryContactName || '';
if(a.length == 0 && b.length == 0)
return 0;
else if(a.length == 0)
return 1;
else if(b.length == 0)
return -1;
else if(a > b)
return 1;
else if(a < b)
return -1;
return 0;
}
Comparison functions must be reflective, transitive, and anti-symmetric. Your function does not satisfy these criteria. For example, if two blank entries are compared with each other, you must return 0, not -1.
this.comparePrimaryContactName = function (a, b)
{
var aName = a.PrimaryContactName;
var bName = b.PrimaryContactName;
return aName === bName ? 0 :
aName.length===0 ? -1 :
bName.length===0 ? 1 :
aName > bName ? 1 : -1;
}
Return 1 instead of -1 for blanks.
this.comparePrimaryContactName = function (a, b) {
if (a.PrimaryContactName == b.PrimaryContactName)
return 0;
if(a.PrimaryContactName == "") return 1;
return a.PrimaryContactName > b.PrimaryContactName ? 1 : -1;
}
Your sort function should return 0 if the two are equal, -1 if a comes before b, and 1 if a comes after b.
See the MDN sort doco for more information.

Sort JavaScript array of Objects based on one of the object's properties [duplicate]

This question already has answers here:
Sort array of objects by string property value
(57 answers)
Closed 8 years ago.
I've got an array of objects, each of which has a property name, a string. I'd like to sort the array by this property. I'd like them sorted in the following way..
`ABC`
`abc`
`BAC`
`bac`
etc...
How would I achieve this in JavaScript?
There are 2 basic ways:
var arr = [{name:"ABC"},{name:"BAC"},{name:"abc"},{name:"bac"}];
arr.sort(function(a,b){
var alc = a.name.toLowerCase(), blc = b.name.toLowerCase();
return alc > blc ? 1 : alc < blc ? -1 : 0;
});
or
arr.sort(function(a,b){
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
Be aware that the 2nd version ignore diacritics, so a and à will be sorted as the same letter.
Now the problem with both these ways is that they will not sort uppercase ABC before lowercase abc, since it will treat them as the same.
To fix that, you will have to do it like this:
arr.sort(function(a,b){
var alc = a.name.toLowerCase(), blc = b.name.toLowerCase();
return alc > blc ? 1 : alc < blc ? -1 : a.name > b.name ? 1 : a.name < b.name ? -1 : 0;
});
Again here you could choose to use localeCompare instead if you don't want diacritics to affect the sorting like this:
arr.sort(function(a,b){
var lccomp = a.name.toLowerCase().localeCompare(b.name.toLowerCase());
return lccomp ? lccomp : a.name > b.name ? 1 : a.name < b.name ? -1 : 0;
});
You can read more about sort here: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/sort
You can pass-in a sort function reference to Array.sort.
objects.sort(function(c, d) {
return (
c['name'].toLowerCase() > d['name'].toLowerCase() ||
c['name'] > d['name']
) ? 1 : -1;
});
see there http://jsfiddle.net/p8Gny/1/
You can pass a custom sorting function to the sort() method of an array. The following will do the trick and take your requirements about capitalization into account.
objects.sort(function(o1, o2) {
var n1 = o1.name, n2 = o2.name, ni1 = n1.toLowerCase(), ni2 = n2.toLowerCase();
return ni1 === ni2 ? (n1 === n2 ? 0 : n1 > n2 ? 1 : -1) : (ni1 > ni2 ? 1 : -1);
});
Slightly modified from Sorting an array of objects,
yourobject.sort(function(a, b) {
var nameA = a.name, nameB = b.name
if (nameA < nameB) //Sort string ascending.
return -1
if (nameA > nameB)
return 1
return 0 //Default return value (no sorting).
})

Categories