I have an array of objects to sort. Each object has two parameters: Strength and Name
objects = []
object[0] = {strength: 3, name: "Leo"}
object[1] = {strength: 3, name: "Mike"}
I want to sort first by Strength and then by name alphabetically. I am using the following code to sort by the first parameter. How do I sort then by the second?
function sortF(ob1,ob2) {
if (ob1.strength > ob2.strength) {return 1}
else if (ob1.strength < ob2.strength){return -1}
return 0;
};
Thanks for your help.
(I am using Array.sort() with the aforementioned sortF as the sort comparison function passed into it.)
Expand your sort function to be like this;
function sortF(ob1,ob2) {
if (ob1.strength > ob2.strength) {
return 1;
} else if (ob1.strength < ob2.strength) {
return -1;
}
// Else go to the 2nd item
if (ob1.name < ob2.name) {
return -1;
} else if (ob1.name > ob2.name) {
return 1
} else { // nothing to split them
return 0;
}
}
A < and > comparison on strings is an alphabetic comparison.
This little function is often handy when sorting by multiple keys:
cmp = function(a, b) {
if (a > b) return +1;
if (a < b) return -1;
return 0;
}
or, more concisely,
cmp = (a, b) => (a > b) - (a < b)
Which works because in javascript:
true - true // gives 0
false - false // gives 0
true - false // gives 1
false - true // gives -1
Apply it like this:
array.sort(function(a, b) {
return cmp(a.strength,b.strength) || cmp(a.name,b.name)
})
Javascript is really missing Ruby's spaceship operator, which makes such comparisons extremely elegant.
You could chain the sort order with logical OR.
objects.sort(function (a, b) {
return a.strength - b.strength || a.name.localeCompare(b.name);
});
When I was looking for an answer to this very question, the answers I found on StackOverflow weren't really what I hoped for. So I created a simple, reusable function that does exactly this. It allows you to use the standard Array.sort, but with firstBy().thenBy().thenBy() style.
https://github.com/Teun/thenBy.js
PS. This is the second time I post this. The first time was removed by a moderator saying "Please don't make promotional posts for your own work". I'm not sure what the rules are here, but I was trying to answer this question. I'm very sorry that it is my own work. Feel free to remove again, but please point me to the rule involved then.
steve's answer, but prettier.
objects.sort(function(a,b)
{
if(a.strength > b.strength) {return 1;}
if(a.strength < b.strength) {return -1;}
if(a.name > b.name ) {return 1;}
if(a.name < b.name ) {return -1;}
return 0;
}
function sortF(ob1,ob2) {
if (ob1.strength > ob2.strength) {return 1}
else if (ob1.strength < ob2.strength) {return -1}
else if (ob1.name > ob2.name) {return 1}
return -1;
};
EDIT: Sort by strength, then if strength is equal, sort by name.
The case where strength and name are equal in both objects doesn't need to be accounted for seperately, since the final return of -1 indicates a less-than-or-equal-to relationship. The outcome of the sort will be correct. It might make it run faster or slower, I don't know. If you want to be explicit, just replace
return -1;
with
else if (ob1.name < ob2.name) {return -1}
return 0;
Find 'sortFn' function below. This function sorts by unlimited number of parameters(such as in c#: SortBy(...).ThenBy(...).ThenByDesc(...)).
function sortFn() {
var sortByProps = Array.prototype.slice.call(arguments),
cmpFn = function(left, right, sortOrder) {
var sortMultiplier = sortOrder === "asc" ? 1 : -1;
if (left > right) {
return +1 * sortMultiplier;
}
if (left < right) {
return -1 * sortMultiplier;
}
return 0;
};
return function(sortLeft, sortRight) {
// get value from object by complex key
var getValueByStr = function(obj, path) {
var i, len;
//prepare keys
path = path.replace('[', '.');
path = path.replace(']', '');
path = path.split('.');
len = path.length;
for (i = 0; i < len; i++) {
if (!obj || typeof obj !== 'object') {
return obj;
}
obj = obj[path[i]];
}
return obj;
};
return sortByProps.map(function(property) {
return cmpFn(getValueByStr(sortLeft, property.prop), getValueByStr(sortRight, property.prop), property.sortOrder);
}).reduceRight(function(left, right) {
return right || left;
});
};
}
var arr = [{
name: 'marry',
LocalizedData: {
'en-US': {
Value: 10000
}
}
}, {
name: 'larry',
LocalizedData: {
'en-US': {
Value: 2
}
}
}, {
name: 'marry',
LocalizedData: {
'en-US': {
Value: 100
}
}
}, {
name: 'larry',
LocalizedData: {
'en-US': {
Value: 1
}
}
}];
document.getElementsByTagName('pre')[0].innerText = JSON.stringify(arr)
arr.sort(sortFn({
prop: "name",
sortOrder: "asc"
}, {
prop: "LocalizedData[en-US].Value",
sortOrder: "desc"
}));
document.getElementsByTagName('pre')[1].innerText = JSON.stringify(arr)
pre {
font-family: "Courier New" Courier monospace;
white-space: pre-wrap;
}
Before:
<pre></pre>
Result:
<pre></pre>
With ES6 you can do
array.sort(function(a, b) {
return SortFn(a.strength,b.strength) || SortFn(a.name,b.name)
})
private sortFn(a, b): number {
return a === b ? 0 : a < b ? -1 : 1;
}
Here is the function I use. It will do an arbitrary number.
function Sorter(){
var self = this;
this.sortDefs = [];
for (let i = 0; i < arguments.length; i++) {
// Runs 5 times, with values of step 0 through 4.
this.sortDefs.push(arguments[i]);
}
this.sort = function(a, b){
for (let i = 0; i < self.sortDefs.length; i++) {
if (a[self.sortDefs[i]] < b[self.sortDefs[i]]) {
return -1;
} else if (a[self.sortDefs[i]] > b[self.sortDefs[i]]) {
return 1
}
}
return 0;
}
}
data.sort(new Sorter('category','name').sort);
In 2018 you can use just sort() ES6 function, that do exactly, what you want.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
Related
Here is a mapping function I was exercising on;
var list = [1,2,3,4,5];
function isOdd(v) {
return v % 2 == 1;
}
function exclude(arr, fn) {
var myList = [];
for (var i = 0; i < arr.length; i++) {
if (fn(arr[i])) {
myList.push(arr[i]);
}
};
return myList;
}
I want to replace for loop with a recursive solution but it doesn't produce a proper array since I started a new list in each function call at line 2. How can I solve it with a recursion? Here is the closest I got;
function exclude(arr, fn) {
let myList = [];
if (arr.length = 1) {
if (fn(arr[0])) {
return myList.push(arr[0]);
}
} else {
return myList.push(exclude(arr.slice(1), fn));
}
}
console.log(exclude(list, isOdd));
Try this one solution. I have changed a bit your implementation.
function exclude(arr, fn, output) {
output || (output = []);
if(!arr.length) {
return output;
}
if (fn(arr[0])) {
output.push(arr[0]);
}
return exclude(arr.slice(1), fn, output);
}
console.log(exclude([1,2,3,4,5,6,7,8,9], function(i) { return i % 2; }));
if (arr.length = 1) {
This assigns 1 to arr.length, effectively trimming the array if there are more than 1 items in it. You probably meant arr.length ===.
Secondly, the Array#push method returns the new length of the array, not the array itself, so where you have:
return myList.push(arr[0]);
You probably want:
myList.push(arr[0]);
return myList;
If you don't want to modify the original array and make copy of the original array on each recursive call, here could be an ugly solution
function exclude(arr, fn, result, index) {
if (!result) {
result = [];
index = -1;
}
index++;
if (index < arr.length && fn(arr[inndex]))
result.push(arr[index]);
if (index === arr.length)
return result;
return exclude(arr, fn, result, index);
}
console.log(exclude([1,2,3,4], function(i){return i%2===0;})); // prints "2,4"
IMHO, your example doesn't really fit with recursion. It is much more readable and less prone to errors to use the for loop here. Recursion can easily mess things up in JS because of contexts and scopes. If you really want to do it as a test case, i recommend you not to transform the array, it will be slow and not of much use. Anyway at the end, you will end with same behaviour as a for loop, but less efficient. here is an example:
var list = [1,2,3,4,5];
function isOdd(v) {
return v % 2 == 1;
}
function exclude(arr, fn, optionalIndex) {
var myList = [];
if(!optionalIndex){
optionalIndex = 0;
}
if(optionalIndex < arr.length){
if(fn(arr[optionalIndex])){
myList.push(arr[optionalIndex]);
}
myList = myList.concat(exclude(arr, fn, optionalIndex + 1));
}
return myList;
}
console.log(exclude(list, isOdd));
It actually would be more interesting for you to try to do it with a real recursion case, for example, using your version with for loop to filter this array with sub-arrays in it:
var list = [1, 2, 3, [0, 1, 2, [0, 10, 20, 30], 4], 5, [1 ,2 ,3]];
Just for fun of it, one line solution:
function exclude(a, fn, c) {
return (c = c || (c !== 0 ? a.length - 1 : c)) >= a.splice(c, fn(a[c]) ? 0:1)*0 ? exclude(a, fn, c - 1) : a;
}
Snippet:
var list = [0,1,2,3,4,5];
function isOdd(v) {
return v % 2 != 0;
}
function exclude(a, fn, c) {
return (c = c || (c !== 0 ? a.length - 1 : c)) >= a.splice(c, fn(a[c]) ? 0:1)*0 ? exclude(a, fn, c - 1) : a;
}
console.log(exclude(list, isOdd));
Since JavaScript doesn't have tail call elimination a recursive solution over a large array will blow your stack. However a functional and recursive approach to the problem would look something like this:
function filter(arr, fn) {
var stopAt = arr.length;
function helper (index, acc) {
return index === stopAt ?
acc :
helper(index+1, fn(arr[index]) ?
acc.concat([arr[index]]) :
acc) ;
}
return helper(0, []);
}
Of course this should never be production code. Having a functional interface while allowing for mutating internal structures is probably the best approach. If you were to look at the source for the functional library Rambda you see that they do mutate internal state.
I have an array with the following structure:
[[],[{"id":1,"meaning":1,"word":"b"},{"id":2,"meaning":1,"word":"a"}],[{"id":3,"meaning":2,"word":"f"},{"id":4,"meaning":2,"word":"c"}],[{"id":5,"meaning":3,"word":"d"}]]
array[0] needs to be empty, because I need that index for a special usage later on.
array[1] for example contains all objects with meaning:1, array[2] all those with meaning:2.
What I want to do now is to sort this array by the first object in the 2D-Array(so to say by the first column).
Thus the output should be like:
[[],[{"id":1,"meaning":1,"word":"b"},{"id":2,"meaning":1,"word":"a"}], [{"id":5,"meaning":3,"word":"d"}], [{"id":3,"meaning":2,"word":"f"},{"id":4,"meaning":2,"word":"c"}]]
I would appreciate all types of answers.
EDIT:
The sort criterion is an ascending alphabetic order
First off, ditch the empty array, as its much easier to add that later than to code around it in your sorting function. Assuming your top level array is called 'arr':
//first we'll sort the sub arrays themselves
var newArr = arr.map(function(subArray) {
subArray.sort(function(a, b) {
if (a.word > b.word) {
return -1;
} else {
return 1;
}
});
return subArray;
})
//and now sort the containing array
.sort(function(a, b) {
if (a[0].word > b[0].word) {
return -1;
} else {
return 1;
}
});
//now we'll add back in your empty array
newArr = [[]].concat(newArr);
if the sub arrays are already in alphebetical order than you only need the global sort.
Did you know that sort may take a function as an argument?
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
Sorting is a comparison, if I understand you correctly you just need to rearrange the objects based on the meaning property.
This should do the trick for you, of course the sorting on word is up to you.
function arrangeByMeaning (arr) {
arr.forEach(function (innerArr, index) {
innerArr.forEach(function (obj, i) {
if (parseInt(obj.meaning) !== index) {
var o = innerArr.pop(i);
arr[index].push(o);
}
});
});
// sort innerArrays on word after rearranging objects
arr.forEach(function (innerArr) {
innerArr.sort(function (a, b) {
return a.word < b.word ? -1 : a.word > b.word ? 1 : 0;
});
});
return arr;
};
This will modify the original array.
Fiddle
Here you go. Bit untidy but works: http://jsfiddle.net/8xv6s9g1/2/
var array = [[],[{"id":1,"meaning":1,"word":"b"},{"id":2,"meaning":1,"word":"a"}],[{"id":3,"meaning":2,"word":"f"},{"id":4,"meaning":2,"word":"c"}],[{"id":5,"meaning":3,"word":"d"}]];
var arrayCopy = array.slice();
for(var i = 0; i < arrayCopy.length; i++) {
arrayCopy[i].sort(function(x, y) {
if(!x || x.word < y.word) return 1;
if(x.word > y.word) return -1;
return 0;
});
}
arrayCopy.sort(function(x, y) {
if(!x.length && !y.length) return 0;
if(!x.length) return -1;
if(!y.length) return 1;
if(x[0].word < y[0].word) return -1;
if(x[0].word > y[0].word) return 1;
return 0;
});
for(var i = 0; i< arrayCopy.length; i++) {
arrayCopy[i].index = i;
}
array.sort(function(x, y) {
if(!x || x.index < y.index) return -1;
if(x.index > y.index) return 1;
return 0;
});
console.log(array);
Output is [[],[{"id":1,"meaning":1,"word":"b"},{"id":2,"meaning":1,"word":"a"}], [{"id":5,"meaning":3,"word":"d"}], [{"id":3,"meaning":2,"word":"f"},{"id":4,"meaning":2,"word":"c"}]] as required
When I call this with [1,2,3,4], it returns undefined and I'm not understanding why. The goal is for it to return true if any combination of numbers in the array add up to the maximum number in the array, and false if it's not possible.
function ArrayAdditionI(arr) {
var max = Math.max.apply(null, arr);
arr.splice(arr.indexOf(max), 1);
var sum = function(arr) { return arr.reduce(function(a,b) { return a + b; }); };
function combos(arr) {
var f = function(prefix, arr) {
for (var i = 0; i < arr.length; i++) {
var clone = prefix.slice(0);
clone.push(arr[i]);
if (sum(clone) == max) { return true; }
return f(clone, arr.slice(i+1));
}
}
return f([], arr);
}
return combos(arr);
}
f is returning undefined when it is called with an empty arr! You will need to explicitly return false if none of the tests in the loop returned from the function. And you must not return false on the first occasion of the loop, but break only when you found true and continue the loop elsewhile.
To fix this, you'd have something like
function combos(arr) {
function f(prefix, arr) {
for (var i = 0; i < arr.length; i++) {
var clone = prefix.slice(0);
clone.push(arr[i]);
if (sum(clone) == max) return true;
if (f(clone, arr.slice(i+1))) return true;
}
return false;
}
return f([], arr);
}
However, also your recursion scheme with the loop looks a bit complicated. I would rather go with the naive enumeration of the "binary tree", where the nodes of each level decide whether the current item will be included in the to-be-tested subset:
function ArrayAdditionI(arr) {
var max = Math.max.apply(null, arr);
arr.splice(arr.indexOf(max), 1);
var sum = function(arr) { return arr.reduce(function(a,b) { return a + b; }, 0); };
function f(subset, arr) {
return arr.length
? f(subset, arr.slice(1)) || f(subset.concat([arr[0]]), arr.slice(1))
: sum(subset) == max
}
return f([], arr);
}
It seems that you don't test all the possible combination.
Here you will test 1+2+3, 2+3, 3 but never 1+3.
Do you really want to have a recursive function here ?
There may be some more simple way to find that.
function ArrayAdditionI(arr) {
var max = Math.max.apply(null, arr);
arr.splice(arr.indexOf(max), 1);
var res = arr.filter(function(num, idx) {
var combination = false;
// Check all combination non previously tested
for (var i = idx; i < arr.length - 1; i++) {
if (num + arr[i+1] === max) {
combination = true;
break;
}
}
return combination;
});
return res.length > 0;
}
The problem is your f function is not ever hitting the
if (sum(clone) == max) { return true; }
line of code so it will just keep recursively calling until arr.length == 0 and it will return undefined.
Your variables torf and results are unused, maybe you forgot to do something with them?
I have a function which sorts by name currently and an array of value / key pairs.
I wonder how can I pass the key on which sort is being performed so I can call the same function every time like so:
var arr = [{name:'bob', artist:'rudy'},
{name:'johhny', artist:'drusko'},
{name:'tiff', artist:'needell'},
{name:'top', artist:'gear'}];
sort(arr, 'name'); //trying to sort by name
sort(arr, 'artist'); //trying to sort by artist
function sort(arr) {
arr.sort(function(a, b) {
var nameA=a.name.toLowerCase(), nameB=b.name.toLowerCase();
if (nameA < nameB) //sort string ascending
return -1;
if (nameA > nameB)
return 1;
return 0; //default return value (no sorting)
});
}
Array.prototype.sortOn = function(key){
this.sort(function(a, b){
if(a[key] < b[key]){
return -1;
}else if(a[key] > b[key]){
return 1;
}
return 0;
});
}
var arr = [{name:'bob', artist:'rudy'},{name:'johhny', artist:'drusko'},{name:'tiff', artist:'needell'},{name:'top', artist:'gear'}];
arr.sortOn("name");
arr.sortOn("artist");
[edit 2020/08/14] This was rather an old answer and not very good as well, so simplified and revised.
Create a function that returns the sorting lambda (the Array.prototype.sort callback that does the actual sorting). That function can receive the key name, the kind of sorting (string (case sensitive or not) or numeric) and the sorting order (ascending/descending). The lambda uses the parameter values (closure) to determine how to sort.
const log = (...strs) =>
document.querySelector("pre").textContent += `\n${strs.join("\n")}`;
const showSortedValues = (arr, key) =>
` => ${arr.reduce((acc, val) => ([...acc, val[key]]), [])}`;
// the actual sort lamda factory function
const sortOnKey = (key, string, desc) => {
const caseInsensitive = string && string === "CI";
return (a, b) => {
a = caseInsensitive ? a[key].toLowerCase() : a[key];
b = caseInsensitive ? b[key].toLowerCase() : b[key];
if (string) {
return desc ? b.localeCompare(a) : a.localeCompare(b);
}
return desc ? b - a : a - b;
}
};
// a few examples
const onNameStringAscendingCaseSensitive =
getTestArray().sort( sortOnKey("name", true) );
const onNameStringAscendingCaseInsensitive =
getTestArray().sort( sortOnKey("name", "CI", true) );
const onValueNumericDescending =
getTestArray().sort( sortOnKey("value", false, true) );
// examples
log(`*key = name, string ascending case sensitive`,
showSortedValues(onNameStringAscendingCaseSensitive, "name")
);
log(`\n*key = name, string descending case insensitive`,
showSortedValues(onNameStringAscendingCaseInsensitive, "name")
);
log(`\n*key = value, numeric desc`,
showSortedValues(onValueNumericDescending, "value")
);
function getTestArray() {
return [{
name: 'Bob',
artist: 'Rudy',
value: 23,
}, {
name: 'John',
artist: 'Drusko',
value: 123,
}, {
name: 'Tiff',
artist: 'Needell',
value: 1123,
}, {
name: 'Top',
artist: 'Gear',
value: 11123,
}, {
name: 'john',
artist: 'Johanson',
value: 12,
}, ];
}
<pre></pre>
function keysrt(key) {
return function(a,b){
if (a[key] > b[key]) return 1;
if (a[key] < b[key]) return -1;
return 0;
}
}
someArrayOfObjects.sort(keysrt('text'));
Make your life easy and use a closure
https://stackoverflow.com/a/31846142/1001405
You can see the working example here
var filter = 'name', //sort by name
data = [{name:'bob', artist:'rudy'},{name:'johhny', artist:'drusko'},{name:'tiff', artist:'needell'},{name:'top', artist:'gear'}];;
var compare = function (filter) {
return function (a,b) { //closure
var a = a[filter],
b = b[filter];
if (a < b) {
return -1;
}else if (a > b) {
return 1;
} else {
return 0;
}
};
};
filter = compare(filter); //set filter
console.log(data.sort(filter));
Looking at all the answers, I came up with my own solution that works cross-browser. The accepted solution does not work in IE or Safari. Also, the other solutions do not allow for sorting by descending.
/*! FUNCTION: ARRAY.KEYSORT(); **/
Array.prototype.keySort = function(key, desc){
this.sort(function(a, b) {
var result = desc ? (a[key] < b[key]) : (a[key] > b[key]);
return result ? 1 : -1;
});
return this;
}
var arr = [{name:'bob', artist:'rudy'}, {name:'johhny', artist:'drusko'}, {name:'tiff', artist:'needell'}, {name:'top', artist:'gear'}];
arr.keySort('artist');
arr.keySort('artist', true);
For example, Hello World! and Hi World! - the first occurrence of the difference is at the second character. What would be the JavaScript/jQuery function?
Assuming, like other answers, that matching strings return -1:
// Find common prefix of strings a and b.
var prefix = function(a,b){
return a && a[0] === b[0] ? a[0] + prefix(a.slice(1), b.slice(1)) : '';
};
// Find index of first difference.
var diff = function(a,b){
return a===b ? -1 : prefix(a,b).length;
};
var tests = [
['Hello World!', 'Hi World!'],
['aaabab', 'aaabzbzz'],
['', ''],
['abc', 'abc'],
['qrs', 'tu'],
['abc', ''],
['', 'abc']
];
console.log('diff', tests.map(test => diff(test[0], test[1])));
// Or just count up to the first difference
// Trickier nested ternary to handle the -1 however.
var diff2 = function(a,b){
return a === b ? -1 : a[0] === b[0] ? 1 + diff2(a.slice(1), b.slice(1)) : 0;
};
console.log('diff2', tests.map(test => diff2(test[0], test[1])));
Maybe something like this? It returns, in that order, the position of the first
difference if there's any, the length of the shortest string if those are different, or -1 if everything is equal.
function findDiff(a, b) {
a = a.toString();
b = b.toString();
for (var i = 0; i < Math.min(a.length, b.length); i++) {
if (a.charAt(i) !== b.charAt(i)) { return i; }
}
if (a.length !== b.length) { return Math.min(a.length, b.length); }
return -1;
}
Thanks Phil for the suggestions!
function strDiff(first, second) {
if(first==second)
return -1;
first = first.toString();
second = second.toString();
var minLen = min(first.length,second.length);
for(var i = 0; i<minLen; i++) {
if(first.charAt(i) != second.charAt(i)) {
return i;
}
}
return minLen;
}
Returns -1 if the strings do not differ, or the index (starting at 0) of the character at which they do (this is the length of the shortest string if they only differ by being different lengths, e.g. 'abcd' and 'abcdef' would return 4.
function firstDiff(a, b) {
var i = 0;
while (a.charAt(i) === b.charAt(i))
if (a.charAt(i++) === '')
return -1;
return i;
}
Returns the position where the two strings a and b first differ or -1 if they are equal.
A more efficient but less readable version:
function firstDiff(a, b) {
for (var i = 0, c; (c = a.charAt(i)) === b.charAt(i); ++i)
if (c === '')
return -1;
return i;
}
If you feel that you should first stringify the arguments, then do it in the invocation:
firstDiff(toString(a), toString(b))
Most often that will be a waste of time. Know your data!