Making compare() function more elegant - javascript

I have this code:
function compare (a, b) {
let comparison = 0;
if (a.essentialsPercentage < b.essentialsPercentage) {
comparison = 1;
} else if (a.essentialsPercentage > b.essentialsPercentage) {
comparison = -1;
} else {
if (a.skillsNicePercentage < b.skillsNicePercentage) {
comparison = 1;
} else if (a.skillsNicePercentage > b.skillsNicePercentage) {
comparison = -1;
} else {
if (a.startDate > b.startDate) {
comparison = 1
} else if (a.startDate < b.startDate) {
comparison = -1
}
}
}
return comparison;
}
What would be the most elegant way of writing it? It doesn't seems nice at the moment.

Assuming this is being used as the comparison function for Array.prototype.sort(), only the sign of the result matters, it doesn't have to be specifically -1 or 1. So instead of if and else, you can simply subtract the numbers.
compare(a, b) {
let comparison = b.essentialPercentage - a.essentialPercentage;
if (comparison == 0) {
comparison = b.skillsNicePercentage - a.skillsNicePercentage;
if (comparison == 0) {
comparison = a.startDate - b.startDate;
}
}
return comparison;
}
If any of the properties are strings rather than numbers, you can use localCompare instead of subtraction.

This tiny function (or the equivalent <=> operator) is perhaps the most obvious lack in the js standard library:
// returns 1 if a > b, -1 if a < b, 0 if a == b
let cmp = (a, b) => (a > b) - (a < b)
Once you have defined it, chained comparisons are very easy:
compare = (a, b) =>
cmp(a.essentialsPercentage, b.essentialsPercentage)
|| cmp(a.skillsNicePercentage, b.skillsNicePercentage)
|| cmp(a.startDate, b.startDate)

If you wanted to you could use a switch statement to keep each one of your cases nice and organized. This would be a "cleaner" method but not necessarily the most correct method. See this thread --> https://stackoverflow.com/a/2312837/11263228
Alternatively, you could create separate functions that will check each of your cases. For example, having a function that takes in a.skillsNicePercentage and b.skillsNicePercentage as parameters and returns true/false. This would be cleaner and also reusable!

You could generalize this in a simpler function that takes an array of field names and sorts the overall array based on those fields, in order. The code below takes some inspiration from the Mongoose database library and allows the - prefix on a field name to sort descending instead of the default ascending order. It also only works for numbers and strings; if you want to support other types, you'll have to extend the code.
function multiSort(fields) {
return (a,b) => {
let result = 0;
for (let i = 0; result === 0 && i < fields.length; ++i) {
let fieldName = fields[i];
if (fieldName.charAt(0) === '-') {
fieldName = fieldName.substring(1);
if (typeof a[fieldName] === 'string') {
result = b[fieldName].localeCompare(a[fieldName]);
}
else {
result = b[fieldName] - a[fieldName];
}
} else {
if (typeof a[fieldName] === 'string') {
result = a[fieldName].localeCompare(b[fieldName]);
}
else {
result = a[fieldName] - b[fieldName];
}
}
}
return result;
};
}
This higher-order function will take an array of fields and return a function that sorts by those fields, in order, for strings and numbers, optionally with a field name prepended by - to sort that field in descending order. You'd use it like this:
someArrayValue.sort(multisort(['essentialsPercentage', 'skillsNicePercentage', '-startDate']));

Based on the logic in your comparison, this should work
function compare (a, b) {
let comparison = a.skillsNicePercentage == b.skillsNicePercentage ? (a.startDate - b.startDate) : b.skillsNicePercentage - a.skillsNicePercentage
let comparison1 = a.essentialsPercentage == b.essentialsPercentage ? b.skillsNicePercentage - a.skillsNicePercentage : comparison
return comparison1;
}

Try
function compare(a, b) {
let c= b.essentialsPercentage - a.essentialsPercentage;
let d= b.skillsNicePercentage - a.skillsNicePercentage;
let e= a.startDate - b.startDate
return Math.sign(c||d||e);
}
function compareNew(a, b) {
let c= b.essentialsPercentage - a.essentialsPercentage;
let d= b.skillsNicePercentage - a.skillsNicePercentage;
let e= a.startDate - b.startDate
return Math.sign(c||d||e);
}
function compareOld(a, b) {
let comparison = 0;
if (a.essentialsPercentage < b.essentialsPercentage) {
comparison = 1;
} else if (a.essentialsPercentage > b.essentialsPercentage) {
comparison = -1;
} else {
if (a.skillsNicePercentage < b.skillsNicePercentage) {
comparison = 1;
} else if (a.skillsNicePercentage > b.skillsNicePercentage) {
comparison = -1;
} else {
if (a.startDate > b.startDate) {
comparison = 1
} else if (a.startDate < b.startDate) {
comparison = -1
}
}
}
return comparison;
}
// TESTS
a={essentialsPercentage:2,skillsNicePercentage:2,startDate:new Date(0)};
tests=[
{essentialsPercentage:2,skillsNicePercentage:2,startDate:new Date(0)},
{essentialsPercentage:2,skillsNicePercentage:2,startDate:new Date(+10000)},
{essentialsPercentage:2,skillsNicePercentage:2,startDate:new Date(-10000)},
{essentialsPercentage:2,skillsNicePercentage:2,startDate:new Date()},
{essentialsPercentage:2,skillsNicePercentage:3,startDate:new Date()},
{essentialsPercentage:2,skillsNicePercentage:1,startDate:new Date()},
{essentialsPercentage:3,skillsNicePercentage:1,startDate:new Date()},
{essentialsPercentage:1,skillsNicePercentage:1,startDate:new Date()},
]
tests.map(b=> console.log(`old: ${compareNew(a,b)} new:${ compareOld(a,b)}`));

Related

How to define default JavaScript sort callback function?

I'm attempting to create my own sort function (question Async version of sort function in JavaScript). I've taken merge sort function from Rosetta Code and make it async:
// based on: https://rosettacode.org/wiki/Sorting_algorithms/Merge_sort#JavaScript
async function mergeSort(fn, array) {
if (array.length <= 1) {
return array;
}
const mid = Math.floor(array.length / 2),
left = array.slice(0, mid), right = array.slice(mid);
await mergeSort(fn, left)
await mergeSort(fn, right)
let ia = 0, il = 0, ir = 0;
while (il < left.length && ir < right.length) {
array[ia++] = (await fn(left[il], right[ir]) <= 0) ? left[il++] : right[ir++];
}
while (il < left.length) {
array[ia++] = left[il++];
}
while (ir < right.length) {
array[ia++] = right[ir++];
}
return array;
}
But I'm not sure how I can define default function fn to work the same as in JavaScript.
console.log([1, 2, 3, 10, 11, 100, 20].sort());
What should be the default sorting function to match those in the JavaScript engine?
Should I convert numbers to strings and compare those? What is the proper implementation?
Updated Answer
The default sort method as defined in core.js looks like so
var getSortCompare = function (comparefn) {
return function (x, y) {
if (y === undefined) return -1;
if (x === undefined) return 1;
if (comparefn !== undefined) return +comparefn(x, y) || 0;
return toString(x) > toString(y) ? 1 : -1;
};
Taken from this repo: https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.array.sort.js
Based on #jonrsharpe comments I was able to implement proper default function:
function defaultSortFn(a, b) {
if (typeof a !== 'string') {
a = String(a);
}
if (typeof b !== 'string') {
b = String(b);
}
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
that can be used in my sort:
Array.prototype.sort = function(fn = defaultSortFn) {
return mergeSort(fn, this);
};
The ECMAScript specification for Arra.prototype.sort mentions that the comparison (in absence of a comparator) involves:
e. Let xString be ? ToString(x).
f. Let yString be ? ToString(y).
g. Let xSmaller be ! IsLessThan(xString, yString, true).
h. If xSmaller is true, return -1𝔽.
i. Let ySmaller be ! IsLessThan(yString, xString, true).
j. If ySmaller is true, return 1𝔽.
k. Return +0𝔽.
Given that the IsLessThan procedure is also executed when two strings are compared with the < operator, we can faithfully replicate the default callback function as follows:
function (x, y) {
let xString = String(x);
let yString = String(y);
return xString < yString ? -1 : yString < xString ? 1 : 0;
}

Javascript custom sort with special characters dash (-) and underscore (_)

I am trying to do a custom sort like below order
special character ( - first, _ last)
digit
alphabets
For example, if I sort below
var words = ['MBC-PEP-1', 'MBC-PEP01', 'MBC-PEP91', 'MBC-PEPA1', 'MBC-PEPZ1', 'MBC-PEP_1'];
result should be
MBC-PEP-1,MBC-PEP_1,MBC-PEP01,MBC-PEP91,MBC-PEPA1,MBC-PEPZ1
by using my code the result is below
"MBC-PEP-1", "MBC-PEP01", "MBC-PEP91", "MBC-PEP_1", "MBC-PEPA1", "MBC-PEPZ1"
but I need the above sorting order, not sure how to achieve it.
function MySort(alphabet)
{
return function(a, b) {
var lowerA = a.toLowerCase()
var lowerB = b.toLowerCase()
var index_a = alphabet.indexOf(lowerA[0]),
index_b = alphabet.indexOf(lowerB[0]);
if (index_a === index_b) {
// same first character, sort regular
if (a < b) {
return -1;
} else if (a > b) {
return 1;
}
return 0;
} else {
return index_a - index_b;
}
}
}
var items = ['MBC-PEP-1', 'MBC-PEP01', 'MBC-PEP91', 'MBC-PEPA1', 'MBC-PEPZ1', 'MBC-PEP_1'],
sorter = MySort('-_0123456789abcdefghijklmnopqrstuvwxyz');
console.log(items.sort(sorter));
I ported an answer from here to JavaScript, which does what you want without using recursion or anything overly complicated:
function MySort(alphabet) {
return function (a, b) {
a = a.toLowerCase();
b = b.toLowerCase();
var pos1 = 0;
var pos2 = 0;
for (var i = 0; i < Math.min(a.length, b.length) && pos1 == pos2; i++) {
pos1 = alphabet.indexOf(a[i]);
pos2 = alphabet.indexOf(b[i]);
}
if (pos1 == pos2 && a.length != b.length) {
return o1.length - o2.length;
}
return pos1 - pos2;
};
}
var items = ['MBC-PEP-1', 'MBC-PEP01', 'MBC-PEP91', 'MBC-PEPA1', 'MBC-PEPZ1', 'MBC-PEP_1'],
sorter = MySort('-_0123456789abcdefghijklmnopqrstuvwxyz');
console.log(items.sort(sorter));
As Narigo said in their answer, you're only comparing the first character. Here's a different idea that's probably simpler:
function MySort(a, b) {
a = a.replace("_", ".");
b = b.replace("_", ".");
return a.localeCompare(b);
}
var items = ['MBC-PEP-1', 'MBC-PEP01', 'MBC-PEP91', 'MBC-PEPA1', 'MBC-PEPZ1', 'MBC-PEP_1'];
console.log(items.sort(MySort));
We're basically using the normal string comparison, except we change the underscore to a dot to decide the ordering, since it's compatible with what you're trying to achieve.
You are only looking at the first character in your algorithm. You need to check more of your string / the next characters as well. Here is a quick solution using recursion:
function MySort(alphabet)
{
return function recSorter(a, b) {
var lowerA = a.toLowerCase()
var lowerB = b.toLowerCase()
var index_a = alphabet.indexOf(lowerA[0]),
index_b = alphabet.indexOf(lowerB[0]);
if (index_a === index_b && index_a >= 0) {
return recSorter(a.slice(1), b.slice(1));
} else {
return index_a - index_b;
}
}
}
var items = ['MBC-PEP-1', 'MBC-PEP01', 'MBC-PEP91', 'MBC-PEPA1', 'MBC-PEPZ1', 'MBC-PEP_1'],
sorter = MySort('-_0123456789abcdefghijklmnopqrstuvwxyz');
console.log(items.sort(sorter));
I'm not sure what you want to happen when you have different lengths of strings, characters outside the alphabet or at other edge cases. For the posted example, this results in the expected order.

Javascript custom sort function for dates with blanks at the bottom

I am using a table plugin (ng-grid) to display a bunch of data which includes some date columns. The plugin allows custom sorting with a "sortingAlgorithm" function, which accepts "aDate" and "bDate" parameters. The dates are stored as strings in the database, and I'm using Moment to convert them to date objects. Here is the sort function:
sortingAlgorithm: function (aDate, bDate) {
var a = moment(aDate, 'MM/DD/YYYY');
var b = moment(bDate, 'MM/DD/YYYY');
if (a.isBefore(b)) {
return -1;
}
else if (a.isAfter(b)) {
return 1;
}
else {
return 0;
}
}
This works fine, but if there is no date, just an empty string, the blanks are going at the end of the list when sorted in ascending order, but at the beginning when sorted descending. What can I do to ensure that blanks ("") are always moved to the bottom of the list?
Thanks.
UPDATE
I think this has something to do specifically with the UI-Grid library. See rowSorter.js. It seems to handle nulls internally, which is gumming up my mojo. I also don't see where the selected sort direction is exposed for me to work with...
I'm adding the "angular-ui-grid" tag...
So I took a copy of ui-grid's internal date-specific sorting function, and replaced their "handleNulls" function call with my own (see below):
sortingAlgorithm: function (a, b) {
var nulls = handleNulls(a, b);
if ( nulls !== null ){
return nulls;
} else {
if (!(a instanceof Date)) {
a = new Date(a);
}
if (!(b instanceof Date)){
b = new Date(b);
}
var timeA = a.getTime(),
timeB = b.getTime();
return timeA === timeB ? 0 : (timeA < timeB ? -1 : 1);
}
}
And here is a copy of ui-grid's "handleNulls" function, updated to always force nulls to the bottom based on the direction:
function handleNulls(a, b) {
if ((!a && a !== 0 && a !== false) || (!b && b !== 0 && b !== false)) {
if ((!a && a !== 0 && a !== false) && (!b && b !== 0 && b !== false)) {
return 0;
}
else if (!a && a !== 0 && a !== false && vm.direction === 'asc') {
return 1;
}
else if (!b && b !== 0 && b !== false && vm.direction === 'asc') {
return -1;
}
else if (!a && a !== 0 && a !== false && vm.direction === 'desc') {
return -1;
}
else if (!b && b !== 0 && b !== false && vm.direction === 'desc') {
return 1;
}
}
return null;
};
vm.direction comes from ui-grid's onRegisterApi callback, which has a hook for sort changes:
onRegisterApi: function (gridApi) {
vm.gridApi = gridApi;
vm.gridApi.core.on.sortChanged($scope, function(grid, sortColumns) {
if (sortColumns[0]) {
vm.direction = sortColumns[0].sort.direction;
} else {
vm.direction = 'none';
}
});
}
Works like a charm!
Here is what I came up with based on this and another thread using moment.js
var sortmedate = function (a, b, rowA, rowB, direction) {
//the dates do not get sorted properly so we need moment.js and this method.
var dateFormat = 'MM/DD/YYYY'; //todo: pass in date format from mvc controller.
if (!a && !b) {
return 0;
}
if (!a) {
return 1;
}
if (!b) {
return -1;
}
var firstDate = moment(a, dateFormat);
if (!firstDate.isValid()) {
return -1;
}
var secondDate = moment(b, dateFormat);
if (!secondDate.isValid()) {
return 1;
}
if (firstDate.isSame(secondDate)) {
return 0;
} else {
return firstDate.isBefore(secondDate) ? -1 : 1;
}
};
Try this:
var direction=1;
var sortingAlgorithm= function (aDate, bDate) {
var a=moment(aDate,'MM/DD/YYYY');
var b=moment(bDate,'MM/DD/YYYY');
if (!a.isValid()) return 1;
if (!b.isValid()) return -1;
return direction*((+a)-(+b));
}
It takes into account the validity of the date and direction (1 or -1), so that the invalid dates are always at the bottom.
If the ng-grid plugin uses the sorting algorithm and then applies a reverse() to sort in descending order, then I don't think you can force the items at the end. You would have to overwrite the sorting in the plugin I guess.
In order to test for empty string, you could put your comparison condition first and so '' is always at the end. This way you are not creating an invalid date by forcing moment.js to parse an empty string.
sortingAlgorithm: function(aDate, bDate) {
if (aDate === '') {
return 1;
}
if (bDate === '') {
return -1;
}
var a = moment(aDate, 'MM/DD/YYYY');
var b = moment(bDate, 'MM/DD/YYYY');
if (a.isBefore(b)) {
return -1;
} else if (a.isAfter(b)) {
return 1;
} else {
return 0;
}
}

Sort the data having combination of letters and digits has value in array of json in javascript

How to sort the array of json data having letters and digits?
JS:
function sortOn(property) {
return function (a, b) {
if (a[property] < b[property]) {
return -1;
} else if (a[property] > b[property]) {
return 1;
} else {
return 0;
}
}
}
var data = [{"id":1,name:"text10"},
{"id":4,"name":"text1"}, {"id":4,"name":"text19"}, {"id":4,"name":"text2"}, {"id":4,"name":"text20"},
{"id":5,"name":"book"}];
data.sort(sortOn('name'));
console.log(data);// when I print the JSON getting book,text1,text10..
//But I have to want to show the json as book,text1,text2...
Any one can help me how to sort the name thing having both letters and digits
Please find the jsfiddle for reference.
Very simplistic implementation. Sort on either the number in the name and if it doesn't exist, use the full name. Could be sped up by caching the parseInt || name comparison.
var result = data.sort(function ( a, b ) {
var first = parseInt(/\d+/g.exec(a.name), 10) || a.name,
second = parseInt(/\d+/g.exec(b.name), 10) || b.name;
if (first > second) return 1;
else if (first === second) return 0;
else return -1;
return -1
});

sort objects by a property values

I have some objects in an array and want to have them sorted. The objects should be sorted by a metric value. So I have the following function:
objectz.sort(function(a,b){
return b.metric - a.metric;
}
The problem is that some objects have the same property values and the results of the sorting are always different.I want to additionally sort the objects with the same metric value by their name property so I get the same order of objects every time I sort them.
Thx in advance!
objectz.sort(function(a,b){
var result = b.metric - a.metric;
if (!result) return a.name > b.name ? 1 : -1;
return result;
});
Similar to zerkms:
objectz.sort(function(a,b) {
var x = b.metric - a.metric;
return x || b.name - a.name;
});
Seems to be a reverse sort (higher values occur first), is that what you want?
Edit
Note that the - operator is only suitable if the value of 'name' can be converted to a number. Otherwise, use < or >. The sort function should deal with a.name == b.name, which the > opertator on its own won't do, so you need something like:
objectz.sort(function(a,b) {
var x = b.metric - a.metric;
// If b.metric == a.metric
if (!x) {
if (b.name == a.name) {
x = 0;
else if (b.name < a.name) {
x = 1;
else {
x = -1;
}
}
return x;
});
which can be made more concise:
objectz.sort(function(a,b) {
var x = b.metric - a.metric;
if (!x) {
x = (b.name == a.name)? 0 : (b.name < a.name)? 1 : -1;
}
return x;
});
Given that the metric comparison seems to be largest to smallest order, then the ternary exrpession should be:
x = (b.name == a.name)? 0 : (b.name < a.name)? -1 : 1;
if it is required that say Zelda comes before Ann. Also, the value of name should be reduced to all lower case (or all upper case), otherwise 'zelda' and 'Ann' will be sorted in the opposite order to 'Zelda' and 'ann'

Categories

Resources