I have two arrays of objects like below:
items = [{"id":"5","tobuy":"1","name":"pop"},
{"id":"6","tobuy":"1","name":"fish"},
{"id":"7","tobuy":"0","name":"soda"}]
pkgs = [{"item_id":"5","store":"Market","aisle":"3"},
{"item_id":"6","store":"Market","aisle":"2"},
{"item_id":"6","store":"Dept","aisle":"8"},
{"item_id":"7","store":"Market","aisle":"4"}]
I'm trying to sort the items array, but I want to leverage the data in the pkgs array.
The "item_id" field in the pkgs array corresponds to the "id" field in the items array.
For example, I want to sort:
first by "tobuy" in descending order
then by "store"
then by "aisle"
then by "name"
While item_id and id correspond between the two arrays, there is not a 1 to 1 relationship. There could be 0 or more pkgs that correspond to any given item.
(If I had a database, I would just join the tables, but in JavaScript I just have the two related arrays).
I'm not sure how to build the comparator function and pass in the second array.
Thanks for any help.
Perhaps something like this?
items = items.map(function (item, index) {
return {
item: item,
pkg: pkgs[index] //I assumed associated pkgs were at the same index
};
}).sort(function (a, b) {
var pkgA = a.pkg, pkgB = b.pkg, r;
r = +b.item.tobuy - +a.item.tobuy;
if (r !== 0) return r;
r = pkgA.store < pkgB.store? -1 : (pkgA.store === pkgB.store? 0 : 1);
if (r !== 0) return r;
r = +pkgA.aisle - +pkgB.aisle;
if (r !== 0) return r;
return pkgA.name < pkgB.name? -1 : (pkgA.name === pkgB.name? 0 : 1);
}).map(function (item) {
return item.item;
});
Instead of merging the data, you could also create a lookup map that allows to quickly retrieve the associated package directly from the sort function.
E.g.
var pkgsMap = pkgs.reduce(function (res, pkg) {
res[pkg.item_id] = pkg;
return res;
}, {});
Then in the sort function you can do:
var pkgA = pkgsMap[a.id], pkgB = pkgsMap[b.id];
EDIT:
There is actually another field in the pkgs array called "ppu" which
is the price per unit. The lowest ppu is the one that would be used.
You can just build your package map using the following and then use the map in the sort function to retrieve the associated package like described above and implement the sort algorithm.
var pkgsMap = pkgs.sort(function (a, b) {
//not sure what ppu is so I sort it as a string
return a.ppu < b.ppu? -1 : Number(a.ppu > b.ppu);
}).reduce(function (res, pkg) {
if (!(pkg.item_id in res)) res[pkg.item_id] = pkg;
return res;
}, {});
Make a function that generates a comparator, this looks unwieldy but means you can generate any sort order desired
function generateComparator(dict, index, order) {
return function (a, b) {
var i, key, direction,
ai = a[index], av,
bi = b[index], bv;
for (i = 0; i < order.length; ++i) {
key = order[i].key;
direction = +!!order[i].reverse || -1;
if (dict[ai].hasOwnProperty(key)) // if in dict, lookup
av = dict[ai][key];
else // else look up in item
av = a[key];
if (dict[bi].hasOwnProperty(key))
bv = dict[ai][key];
else
bv = b[key];
// console.log(i, key, av, bv, direction); // debug
if (av === bv)
continue;
if (av < bv)
return direction;
return -direction;
}
return 0;
};
}
Convert your Arrays into a dictionary
var dict = (function (arr, index) {
var o = {}, i;
for (i = 0; i < arr.length; ++i) {
o[arr[i][index]] = arr[i];
}
return o;
}(pkgs, 'item_id'));
Define your sort choices
var order = [
{key: 'tobuy', reverse: 1},
{key: 'store'},
{key: 'aisle'},
{key: 'name'}
];
Generate comparator with dictionary
var comparator = generateComparator(dict, 'id', order);
Then sort your first Array
items.sort(comparator);
/* [
{"id": "6", "tobuy": "1", "name": "fish"},
{"id": "5", "tobuy": "1", "name": "pop"},
{"id": "7", "tobuy": "0", "name": "soda"}
] */
Let's consider how you would do this in SQL:
SELECT * FROM items INNER JOIN pkgs ON items.id = pkgs.item_id
ORDER BY tobuy DESC, store, aisle, name
The following answer demonstrates how to implement an inner join and an equijoin in JavaScript:
function equijoin(primary, foreign, primaryKey, foreignKey, select) {
var m = primary.length, n = foreign.length, index = [], c = [];
for (var i = 0; i < m; i++) { // loop through m items
var row = primary[i];
index[row[primaryKey]] = row; // create an index for primary table
}
for (var j = 0; j < n; j++) { // loop through n items
var y = foreign[j];
var x = index[y[foreignKey]]; // get corresponding row from primary
c.push(select(x, y)); // select only the columns you need
}
return c;
}
Now you can use equijoin to join items and pkgs as follows:
equijoin(items, pkgs, "id", "item_id", function (item, pkg) {
return {
id: +item.id,
tobuy: +item.tobuy,
store: pkg.store,
aisle: +pkg.aisle,
name: item.name
};
});
Note that I'm coercing item.id, item.tobuy and pkg.aisle to numbers by applying the unary + operator to them.
Now that we joined the two tables we need to sort them. To sort the table we use the built-in array sort method:
.sort(function (a, b) {
// ORDER BY tobuy DESC
var aTobuy = a.tobuy, bTobuy = b.tobuy;
if (aTobuy < bTobuy) return 1;
if (aTobuy > bTobuy) return -1;
// ORDER BY store
var aStore = a.store, bStore = b.store;
if (aStore < bStore) return -1;
if (aStore > bStore) return 1;
// ORDER BY aisle
var aAisle = a.aisle, bAisle = b.aisle;
if (aAisle < bAisle) return -1;
if (aAisle > bAisle) return 1;
// ORDER BY name
var aName = a.name, bName = b.name;
if (aName < bName) return -1;
if (aName > bName) return 1;
// keep them unchanged
return a.id - b.id;
});
The sort method is unstable (i.e. it might not preserve the ordering of items with equal sort value in the input list). Hence to workaround this limitation we return a.id - b.id as the last statement.
Also notice that we're comparing all the values (whether strings or numbers) using < and >. Strings are compared lexicographically while numbers are compared numerically.
Put together the code is as follows:
var table = equijoin(items, pkgs, "id", "item_id", function (item, pkg) {
return {
id: +item.id,
tobuy: +item.tobuy,
store: pkg.store,
aisle: +pkg.aisle,
name: item.name
};
}).sort(function (a, b) {
var aTobuy = a.tobuy, bTobuy = b.tobuy;
if (aTobuy < bTobuy) return 1;
if (aTobuy > bTobuy) return -1;
var aStore = a.store, bStore = b.store;
if (aStore < bStore) return -1;
if (aStore > bStore) return 1;
var aAisle = a.aisle, bAisle = b.aisle;
if (aAisle < bAisle) return -1;
if (aAisle > bAisle) return 1;
var aName = a.name, bName = b.name;
if (aName < bName) return -1;
if (aName > bName) return 1;
return a.id - b.id;
});
Not as concise as SQL is it? Anyway, see the demo for yourself: http://jsfiddle.net/7ZG96/
Edit: If you want only want the id, tobuy and name columns then you can extract it from the sorted table using map as follows:
table.map(function (item) {
return {
id: item.id,
tobuy: item.tobuy,
name: item.name
};
});
This corresponds to the following SQL query:
SELECT id, tobuy, name FROM (SELECT * FROM items INNER JOIN pkgs
ON items.id = pkgs.item_id ORDER BY tobuy DESC, store, aisle, name)
See the updated demo: http://jsfiddle.net/7ZG96/1/
Related
I have an array of following strings:
['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0']
...etc.
I need a solution that will give me following ordered result
['4.5.0', '4.21.0', '4.22.0', '5.1.0', '5.5.1', '6.1.0'].
I tried to implement a sort so it first sorts by the numbers in the first position, than in case of equality, sort by the numbers in the second position (after the first dot), and so on...
I tried using sort() and localeCompare(), but if I have elements '4.5.0' and '4.11.0', I get them sorted as ['4.11.0','4.5.0'], but I need to get ['4.5.0','4.11.0'].
How can I achieve this?
You could prepend all parts to fixed size strings, then sort that, and finally remove the padding again.
var arr = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
arr = arr.map( a => a.split('.').map( n => +n+100000 ).join('.') ).sort()
.map( a => a.split('.').map( n => +n-100000 ).join('.') );
console.log(arr)
Obviously you have to choose the size of the number 100000 wisely: it should have at least one more digit than your largest number part will ever have.
With regular expression
The same manipulation can be achieved without having to split & join, when you use the callback argument to the replace method:
var arr = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
arr = arr.map( a => a.replace(/\d+/g, n => +n+100000 ) ).sort()
.map( a => a.replace(/\d+/g, n => +n-100000 ) );
console.log(arr)
Defining the padding function once only
As both the padding and its reverse functions are so similar, it seemed a nice exercise to use one function f for both, with an extra argument defining the "direction" (1=padding, -1=unpadding). This resulted in this quite obscure, and extreme code. Consider this just for fun, not for real use:
var arr = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
arr = (f=>f(f(arr,1).sort(),-1)) ((arr,v)=>arr.map(a=>a.replace(/\d+/g,n=>+n+v*100000)));
console.log(arr);
Use the sort compare callback function
You could use the compare function argument of sort to achieve the same:
arr.sort( (a, b) => a.replace(/\d+/g, n => +n+100000 )
.localeCompare(b.replace(/\d+/g, n => +n+100000 )) );
But for larger arrays this will lead to slower performance. This is because the sorting algorithm will often need to compare a certain value several times, each time with a different value from the array. This means that the padding will have to be executed multiple times for the same number. For this reason, it will be faster for larger arrays to first apply the padding in the whole array, then use the standard sort, and then remove the padding again.
But for shorter arrays, this approach might still be the fastest. In that case, the so-called natural sort option -- that can be achieved with the extra arguments of localeCompare -- will be more efficient than the padding method:
var arr = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
arr = arr.sort( (a, b) => a.localeCompare(b, undefined, { numeric:true }) );
console.log(arr);
More about the padding and unary plus
To see how the padding works, look at the intermediate result it generates:
[ "100005.100005.100001", "100004.100021.100000", "100004.100022.100000",
"100006.100001.100000", "100005.100001.100000" ]
Concerning the expression +n+100000, note that the first + is the unary plus and is the most efficient way to convert a string-encoded decimal number to its numerical equivalent. The 100000 is added to make the number have a fixed number of digits. Of course, it could just as well be 200000 or 300000. Note that this addition does not change the order the numbers will have when they would be sorted numerically.
The above is just one way to pad a string. See this Q&A for some other alternatives.
If you are looking for a npm package to compare two semver version, https://www.npmjs.com/package/compare-versions is the one.
Then you can sort version like this:
// ES6/TypeScript
import compareVersions from 'compare-versions';
var versions = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
var sorted = versions.sort(compareVersions);
You could split the strings and compare the parts.
function customSort(data, order) {
function isNumber(v) {
return (+v).toString() === v;
}
var sort = {
asc: function (a, b) {
var i = 0,
l = Math.min(a.value.length, b.value.length);
while (i < l && a.value[i] === b.value[i]) {
i++;
}
if (i === l) {
return a.value.length - b.value.length;
}
if (isNumber(a.value[i]) && isNumber(b.value[i])) {
return a.value[i] - b.value[i];
}
return a.value[i].localeCompare(b.value[i]);
},
desc: function (a, b) {
return sort.asc(b, a);
}
}
var mapped = data.map(function (el, i) {
return {
index: i,
value: el.split('')
};
});
mapped.sort(sort[order] || sort.asc);
return mapped.map(function (el) {
return data[el.index];
});
}
var array = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0'];
console.log('sorted array asc', customSort(array));
console.log('sorted array desc ', customSort(array, 'desc'));
console.log('original array ', array);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can check in loop if values are different, return difference, else continue
var a=['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
a.sort(function(a,b){
var a1 = a.split('.');
var b1 = b.split('.');
var len = Math.max(a1.length, b1.length);
for(var i = 0; i< len; i++){
var _a = +a1[i] || 0;
var _b = +b1[i] || 0;
if(_a === _b) continue;
else return _a > _b ? 1 : -1
}
return 0;
})
console.log(a)
Though slightly late this would be my solution;
var arr = ["5.1.1","5.1.12","5.1.2","3.7.6","2.11.4","4.8.5","4.8.4","2.10.4"],
sorted = arr.sort((a,b) => {var aa = a.split("."),
ba = b.split(".");
return +aa[0] < +ba[0] ? -1
: aa[0] === ba[0] ? +aa[1] < +ba[1] ? -1
: aa[1] === ba[1] ? +aa[2] < +ba[2] ? -1
: 1
: 1
: 1;
});
console.log(sorted);
Here's a solution I developed based on #trincot's that will sort by semver even if the strings aren't exactly "1.2.3" - they could be i.e. "v1.2.3" or "2.4"
function sortSemVer(arr, reverse = false) {
let semVerArr = arr.map(i => i.replace(/(\d+)/g, m => +m + 100000)).sort(); // +m is just a short way of converting the match to int
if (reverse)
semVerArr = semVerArr.reverse();
return semVerArr.map(i => i.replace(/(\d+)/g, m => +m - 100000))
}
console.log(sortSemVer(["1.0.1", "1.0.9", "1.0.10"]))
console.log(sortSemVer(["v2.1", "v2.0.9", "v2.0.12", "v2.2"], true))
This seems to work provided there are only digits between the dots:
var a = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0']
a = a.map(function (x) {
return x.split('.').map(function (x) {
return parseInt(x)
})
}).sort(function (a, b) {
var i = 0, m = a.length, n = b.length, o, d
o = m < n ? n : m
for (; i < o; ++i) {
d = (a[i] || 0) - (b[i] || 0)
if (d) return d
}
return 0
}).map(function (x) {
return x.join('.')
})
'use strict';
var arr = ['5.1.2', '5.1.1', '5.1.1', '5.1.0', '5.7.2.2'];
Array.prototype.versionSort = function () {
var arr = this;
function isNexVersionBigger (v1, v2) {
var a1 = v1.split('.');
var b2 = v2.split('.');
var len = a1.length > b2.length ? a1.length : b2.length;
for (var k = 0; k < len; k++) {
var a = a1[k] || 0;
var b = b2[k] || 0;
if (a === b) {
continue;
} else
return b < a;
}
}
for (var i = 0; i < arr.length; i++) {
var min_i = i;
for (var j = i + 1; j < arr.length; j++) {
if (isNexVersionBigger(arr[i], arr[j])) {
min_i = j;
}
}
var temp = arr[i];
arr[i] = arr[min_i];
arr[min_i] = temp;
}
return arr;
}
console.log(arr.versionSort());
This solution accounts for version numbers that might not be in the full, 3-part format (for example, if one of the version numbers is just 2 or 2.0 or 0.1, etc).
The custom sort function I wrote is probably mostly what you're looking for, it just needs an array of objects in the format {"major":X, "minor":X, "revision":X}:
var versionArr = ['5.5.1', '4.21.0', '4.22.0', '6.1.0', '5.1.0', '4.5.0'];
var versionObjectArr = [];
var finalVersionArr = [];
/*
split each version number string by the '.' and separate them in an
object by part (major, minor, & revision). If version number is not
already in full, 3-part format, -1 will represent that part of the
version number that didn't exist. Push the object into an array that
can be sorted.
*/
for(var i = 0; i < versionArr.length; i++){
var splitVersionNum = versionArr[i].split('.');
var versionObj = {};
switch(splitVersionNum.length){
case 1:
versionObj = {
"major":parseInt(splitVersionNum[0]),
"minor":-1,
"revision":-1
};
break;
case 2:
versionObj = {
"major":parseInt(splitVersionNum[0]),
"minor":parseInt(splitVersionNum[1]),
"revision":-1
};
break;
case 3:
versionObj = {
"major":parseInt(splitVersionNum[0]),
"minor":parseInt(splitVersionNum[1]),
"revision":parseInt(splitVersionNum[2])
};
}
versionObjectArr.push(versionObj);
}
//sort objects by parts, going from major to minor to revision number.
versionObjectArr.sort(function(a, b){
if(a.major < b.major) return -1;
else if(a.major > b.major) return 1;
else {
if(a.minor < b.minor) return -1;
else if(a.minor > b.minor) return 1;
else {
if(a.revision < b.revision) return -1;
else if(a.revision > b.revision) return 1;
}
}
});
/*
loops through sorted object array to recombine it's version keys to match the original string's value. If any trailing parts of the version
number are less than 0 (i.e. they didn't exist so we replaced them with
-1) then leave that part of the version number string blank.
*/
for(var i = 0; i < versionObjectArr.length; i++){
var versionStr = "";
for(var key in versionObjectArr[i]){
versionStr = versionObjectArr[i].major;
versionStr += (versionObjectArr[i].minor < 0 ? '' : "." + versionObjectArr[i].minor);
versionStr += (versionObjectArr[i].revision < 0 ? '' : "." + versionObjectArr[i].revision);
}
finalVersionArr.push(versionStr);
}
console.log('Original Array: ',versionArr);
console.log('Expected Output: ',['4.5.0', '4.21.0', '4.22.0', '5.1.0', '5.5.1', '6.1.0']);
console.log('Actual Output: ', finalVersionArr);
Inspired from the accepted answer, but ECMA5-compatible, and with regular string padding (see my comments on the answer):
function sortCallback(a, b) {
function padParts(version) {
return version
.split('.')
.map(function (part) {
return '00000000'.substr(0, 8 - part.length) + part;
})
.join('.');
}
a = padParts(a);
b = padParts(b);
return a.localeCompare(b);
}
Usage:
['1.1', '1.0'].sort(sortCallback);
const arr = ["5.1.1","5.1.12","5.1.2","3.7.6","2.11.4","4.8.5","4.8.4","2.10.4"];
const sorted = arr.sort((a,b) => {
const ba = b.split('.');
const d = a.split('.').map((a1,i)=>a1-ba[i]);
return d[0] ? d[0] : d[1] ? d[1] : d[2]
});
console.log(sorted);
This can be in an easier way using the sort method without hardcoding any numbers and in a more generic way.
enter code here
var arr = ['5.1.2', '5.1.1', '5.1.1', '5.1.0', '5.7.2.2'];
splitArray = arr.map(elements => elements.split('.'))
//now lets sort based on the elements on the corresponding index of each array
//mapped.sort(function(a, b) {
// if (a.value > b.value) {
// return 1;
// }
// if (a.value < b.value) {
// return -1;
// }
// return 0;
//});
//here we compare the first element with the first element of the next version number and that is [5.1.2,5.7.2] 5,5 and 1,7 and 2,2 are compared to identify the smaller version...In the end use the join() to get back the version numbers in the proper format.
sortedArray = splitArray.sort((a, b) => {
for (i in a) {
if (parseInt(a[i]) < parseInt(b[i])) {
return -1;
break
}
if (parseInt(a[i]) > parseInt(b[i])) {
return +1;
break
} else {
continue
}
}
}).map(p => p.join('.'))
sortedArray = ["5.1.0", "5.1.1", "5.1.1", "5.1.2", "5.7.2.2"]
sort 1.0a notation correct
use native localeCompare to sort 1.090 notation
function log(label,val){
document.body.append(label,String(val).replace(/,/g," - "),document.createElement("BR"));
}
const sortVersions = (
x,
v = s => s.match(/[a-z]|\d+/g).map(c => c==~~c ? String.fromCharCode(97 + c) : c)
) => x.sort((a, b) => (a + b).match(/[a-z]/)
? v(b) < v(a) ? 1 : -1
: a.localeCompare(b, 0, {numeric: true}))
let v=["1.90.1","1.090","1.0a","1.0.1","1.0.0a","1.0.0b","1.0.0.1","1.0a"];
log(' input : ',v);
log('sorted: ',sortVersions(v));
log('no dups:',[...new Set(sortVersions(v))]);
In ES6 you can go without regex.
const versions = ["0.4", "0.11", "0.4.1", "0.4", "0.4.2", "2.0.1","2", "0.0.1", "0.2.3"];
const splitted = versions.map(version =>
version
.split('.')
.map(i => +i))
.map(i => {
let items;
if (i.length === 1) {
items = [0, 0]
i.push(...items)
}
if (i.length === 2) {
items = [0]
i.push(...items)
}
return i
})
.sort((a, b) => {
for(i in a) {
if (a[i] < b[i]) {
return -1;
}
if (a[i] > b[i]) {
return +1;
}
}
})
.map(item => item.join('.'))
const sorted = [...new Set(splitted)]
If ES6 I do this:
versions.sort((v1, v2) => {
let [, major1, minor1, revision1 = 0] = v1.match(/([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/);
let [, major2, minor2, revision2 = 0] = v2.match(/([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/);
if (major1 != major2) return parseInt(major1) - parseInt(major2);
if (minor1 != minor2) return parseInt(minor1) - parseInt(major2);
return parseInt(revision1) - parseInt(revision2);
});
**Sorted Array Object by dotted version value**
var sampleData = [
{ name: 'Edward', value: '2.1.2' },
{ name: 'Sharpe', value: '2.1.3' },
{ name: 'And', value: '2.2.1' },
{ name: 'The', value: '2.1' },
{ name: 'Magnetic', value: '2.2' },
{ name: 'Zeros', value: '0' },
{ name: 'Zeros', value: '1' }
];
arr = sampleData.map( a => a.value).sort();
var requireData = [];
arr.forEach(function(record, index){
var findRecord = sampleData.find(arr => arr.value === record);
if(findRecord){
requireData.push(findRecord);
}
});
console.log(requireData);
[check on jsfiddle.net][1]
[1]: https://jsfiddle.net/jx3buswq/2/
It is corrected now!!!
I need to sort a list of array by a particular word. Any help would be appreciated. Thank you.
List=[microphone,phone,telephone,mobilephone]
word="pho"
Answer should be,
answer=[phone,telephone,microphone,mobilephone]
You can use the indexOf() to get an index number and pass that to the sort method.
const list = ["microphone", "phone", "telephone", "mobilephone"];
const sortBySubstring = (words, match) => {
return words.sort((a, b) => {
return a.indexOf(match) - b.indexOf(match);
});
}
const result = sortBySubstring(list, "pho");
console.log(result);
EDIT:
If there is a word in your list that doesn't contain the substring it will be placed at the beginning of the array. There are some ways to change this behaviour.
First of all you could check if it exists with includes(), if it doesn't exist put it at the end the array
const list = ["microphone", "phone", "telephone", "mobilephone", "telemobile"];
const sortBySubstring = (words, match) => {
return words.sort((a, b) => {
if(!a.includes(match) || !b.includes(match)) return 1;
return a.indexOf(match) - b.indexOf(match);
});
}
const result = sortBySubstring(list, "pho");
console.log(result);
Another option is to filter() out words that don't contain your given substring
const list = ["microphone", "phone", "telephone", "mobilephone", "telemobile"];
const sortBySubstring = (words, match) => {
const contains = words.filter((word) => word.includes(match));
return contains.sort((a, b) => {
return a.indexOf(match) - b.indexOf(match);
});
}
const result = sortBySubstring(list, "pho");
console.log(result);
Little bit longer than the answer by Reyno but it moves strings that don't exist in the array to the end.
// The following function was taken from here: https://stackoverflow.com/questions/16096872/how-to-sort-2-dimensional-array-by-column-value (answer by jahroy)
function compareSecondColumn(a, b) {
if (a[1] === b[1]) {
return 0;
}
else {
return (a[1] < b[1]) ? -1 : 1;
}
}
function sortByWord(list, word) {
// put data into 2d array (x) with array item and indexOf search term
var x = [];
var doesntContainWord = [];
for (var i = 0; i < list.length; i++) {
if (list[i].indexOf(word) == -1) {
// if the search word doesn't exist, push to array
doesntContainWord.push(list[i]);
} else {
x[i] = [list[i], list[i].indexOf(word)];
}
}
doesntContainWord.sort();
// now sort var x by second col.
x.sort(compareSecondColumn);
// now dump values back into array and return them
var toReturn = [];
for (var i = 0; i < x.length; i++) {
toReturn[i] = x[i][0];
}
return toReturn.concat(doesntContainWord);
}
var data = ["microphone", "phone", "telephone", "mobilephone", "cat", "animal"];
console.log(sortByWord(data, "pho"))
I have a dgrid table comprising 13 columns.
The sort in decreasing order works as expected: Highest values, followed by lowest values, followed by entries that have no values.
However, when I sort in ascending order, there are fields with no values, then fields with 0, then fields with no values again and finally fields with values in ascending order.
I have looked into source code, but I am unable to figure out what is causing this.
Is this a bug with dgrid sorting?
What could be the workaround for this?
Provided that your column is read-only, you could add a field in the data source (see displayValue in the code below) reflecting your values, but where strings are replaced with a negative number.
Then, instead of the real field, put only this field in the grid. And display the real value in the get function of the column.
I've used the same workaround to apply a case-insensitive sort in a column populated with proper names.
require(["dgrid/Grid", "dojo/domReady!"], function (Grid) {
var data = [];
for(var i = 0; i < 3; i++) for(j = 1; j <= 2; j++) data.push({value: i});
for(i = 1; i < 3; i++) data.push({value: ""});
data.forEach(function(item) {
item.displayValue = typeof item.value == "string" ? -1 : item.value;
});
var grid = new Grid({
columns: {
displayValue: {
label: "Value",
get: function(item) { return item.value; }
}
}
}, 'grid');
grid.renderArray(data);
grid.set("sort", "displayValue");
});
<script>
dojoConfig = {
async: true,
packages: [
{
name: 'dgrid',
location: 'https://cdn.rawgit.com/SitePen/dgrid/v1.1.0'
}
]
}
</script>
<script src="https://ajax.googleapis.com/ajax/libs/dojo/1.11.2/dojo/dojo.js"></script>
<link rel="stylesheet" href="https://cdn.rawgit.com/SitePen/dgrid/v1.1.0/css/dgrid.css"/>
<style>
.dgrid-row-table {
height: 24px;
}
</style>
<body>
<div id="grid" style="width:200px;height:210px">
</div>
</body>
= Better answer as my last post, after your comments =
AFAIK, dgrid (and also dstore) doesn't allow to use a custom sort function.
The pattern below overcomes this limitation. It uses an OnDemandGrid populated with a dstore.
For instance, the field1 column of this grid contains values ranging from "g1" to "g20". They are not sorted aphanumerically (default sort) but numerically (custom sort based on the numbers after "g") => "g1" < "g2" < "g3" < ... < "g10" < "g11"...
This custom sort is performed by the comparegXX callback function:
var comparegXX = function(a, b) {
// 'compare' is a generic comparison function for values of the same type
try {
return compare(parseInt(a.value.substr(1), 10), parseInt(b.value.substr(1), 10));
}
catch(ex) { return compareMixedTypes(a, b); }
}
... and comparegXX is assigned to field1 in its column definition (sort attribute):
field1: {
sort: comparegXX
},
It' the same for field2 (mixed types sort - strings and numbers - performed by compareMixedTypes).
If you want to assign another custom sort to a field, write your custom comparison callback function and add it in the column definition of the field: fieldX: {sort: myCompareFunction}.
Note that the store shouldn't contain any field named _newPos. This field is created and used by the doSort function. It contains the new relative positions of the data rows after applying the custom sort - the new sort of the grid is based on this field.
var compare = function(a, b) { return a > b ? 1 : a < b ? -1 : 0; }
// comparison functions for custom sorts
// use a.value and b.value in these functions, not directly a and b
var comparegXX = function(a, b) {
try {
return compare(parseInt(a.value.substr(1), 10), parseInt(b.value.substr(1), 10));
}
catch(ex) { return compareMixedTypes(a, b); }
}
var compareMixedTypes = function(a, b) {
var aType = typeof a.value;
return aType == typeof b.value ? compare(a.value, b.value) :
aType == "string" ? -1 : 1;
}
require(["dstore/Memory", "dgrid/OnDemandGrid", "dojo/domReady!"], function (Memory, OnDemandGrid) {
// populate the store (random values in field2)
var tbl = [];
for(var i = 1; i < 21; i++) {
var item = {id: i};
item.field1 = "g" + i;
item.field2 = (i == 1 || Math.random() < 0.2) ? "" : Math.floor(Math.random() * 10);
tbl.push(item);
}
var store = new Memory( {data: tbl });
var grid = new OnDemandGrid({
collection: store,
columns:
{
id: {},
field1: {
sort: comparegXX
},
field2: {
sort: compareMixedTypes
}
},
}, 'grid');
var lastField = null;
var descending = null;
grid.doSort = function(e) {
// custom sort of the grid, replaces the default sort
var field = e.sort[0].property;
if(lastField == field) descending = !descending;
else {
lastField = field;
if(descending == null) descending = e.sort[0].descending;
}
var sortFunc = grid.column(field).sort;
if(sortFunc) {
// calculate the positions of the rows based on the custom compare function,
// they are stored in the _newPos field, and then the grid is sorted on it
var tmp = [], tmp2 = {};
store.forEach(function(item, i) { tmp.push({value: item[field], pos: i}); });
tmp.sort(sortFunc);
tmp.forEach(function(item, i) { tmp2[item.pos] = i; });
store.forEach(function(item, i) { item._newPos = tmp2[i]; });
grid.set("sort", "_newPos", descending);
}
else grid.set("sort", field, descending);
grid.updateSortArrow([{property: field, descending: descending}]);
}
grid.on("dgrid-sort", function(e) {
grid.doSort(e);
e.preventDefault();
});
// initial sort of the grid, use this instead of grid.set("sort"...)
grid.doSort({sort: [{property: "field1", descending: false}]});
grid.startup();
});
<script>
dojoConfig = {
async: true,
packages: [
{
name: 'dgrid',
location: 'https://cdn.rawgit.com/SitePen/dgrid/v1.1.0'
},
{
name: 'dstore',
location: '//cdn.rawgit.com/SitePen/dstore/v1.1.1'
}
]
}
</script>
<script src="https://ajax.googleapis.com/ajax/libs/dojo/1.11.2/dojo/dojo.js"></script>
<link rel="stylesheet" href="https://cdn.rawgit.com/SitePen/dgrid/v1.1.0/css/dgrid.css" />
<body>
<div id="grid" style="width:300px;height:530px;">
</div>
</body>
I have an array of objects like this:
var booksArray = [
{Id:1,Rate:5,Price:200,Name:"History"},
{Id:2,Rate:5,Price:150,Name:"Geographic"},
{Id:3,Rate:1,Price:75,Name:"Maths"},
{Id:4,Rate:2,Price:50,Name:"Statistics"},
{Id:5,Rate:3,Price:120,Name:"Drawing"},
{Id:6,Rate:2,Price:100,Name:"Arts"},
{Id:7,Rate:3,Price:110,Name:"Chemistry"},
{Id:8,Rate:4,Price:160,Name:"Biology"},
{Id:9,Rate:4,Price:90,Name:"Software Engineering"},
{Id:10,Rate:4,Price:80,Name:"Algorithm"},
{Id:11,Rate:1,Price:85,Name:"Urdu"},
{Id:12,Rate:1,Price:110,Name:"International Relations"},
{Id:13,Rate:1,Price:120,Name:"Physics"},
{Id:14,Rate:3,Price:230,Name:"Electronics"},
{Id:15,Rate:3,Price:250,Name:"Jihad"},
{Id:16,Rate:3,Price:200,Name:"Functional Programming"},
{Id:17,Rate:2,Price:190,Name:"Computer Science"},
{Id:18,Rate:2,Price:50,Name:"Problem solving Techniques"},
{Id:19,Rate:6,Price:150,Name:"C#"},
{Id:20,Rate:7,Price:250,Name:"ASP.Net"}
]
I am sorting it with this function:
function sortBy(key, reverse) {
var moveSmaller = reverse ? 1 : -1;
var moveLarger = reverse ? -1 : 1;
return function(a, b) {
if (a[key] < b[key]) {
return moveSmaller;
}
if (a[key] > b[key]) {
return moveLarger;
}
return 0;
};
}
booksArray.sort(sortBy('Rate', false))
console.log(JSON.stringify(booksArray))
And this is producing this result:
[
{Id:3,Rate:1,Price:75,Name:"Maths"},
{Id:11,Rate:1,Price:85,Name:"Urdu"},
{Id:12,Rate:1,Price:110,Name:"International Relations"},
{Id:13,Rate:1,Price:120,Name:"Physics"},
{Id:4,Rate:2,Price:50,Name:"Statistics"},
{Id:6,Rate:2,Price:100,Name:"Arts"},
{Id:17,Rate:2,Price:190,Name:"Computer Science"},
{Id:18,Rate:2,Price:50,Name:"Problem solving Techniques"},
{Id:5,Rate:3,Price:120,Name:"Drawing"},
{Id:7,Rate:3,Price:110,Name:"Chemistry"},
{Id:14,Rate:3,Price:230,Name:"Electronics"},
{Id:15,Rate:3,Price:250,Name:"Jihad"},
{Id:16,Rate:3,Price:200,Name:"Functional Programming"},
{Id:8,Rate:4,Price:160,Name:"Biology"},
{Id:9,Rate:4,Price:90,Name:"Software Engineering"},
{Id:10,Rate:4,Price:80,Name:"Algorithm"},
{Id:1,Rate:5,Price:200,Name:"History"},
{Id:2,Rate:5,Price:150,Name:"Geographic"},
{Id:19,Rate:6,Price:150,Name:"C#"},
{Id:20,Rate:7,Price:250,Name:"ASP.Net"}
]
You can see it is sorting on the Rate field which is fine. Now I want to resort again the parts of array without disturbing Rate sorting. I need output like this:
[
{Id:12,Rate:1,Price:110,Name:"International Relations"},
{Id:3,Rate:1,Price:75,Name:"Maths"},
{Id:13,Rate:1,Price:120,Name:"Physics"},
{Id:11,Rate:1,Price:85,Name:"Urdu"},
{Id:6,Rate:2,Price:100,Name:"Arts"},
{Id:17,Rate:2,Price:190,Name:"Computer Science"},
{Id:18,Rate:2,Price:50,Name:"Problem solving Techniques"},
{Id:4,Rate:2,Price:50,Name:"Statistics"},
{Id:7,Rate:3,Price:110,Name:"Chemistry"},
{Id:5,Rate:3,Price:120,Name:"Drawing"},
{Id:14,Rate:3,Price:230,Name:"Electronics"},
{Id:16,Rate:3,Price:200,Name:"Functional Programming"},
{Id:15,Rate:3,Price:250,Name:"Jihad"},
{Id:10,Rate:4,Price:80,Name:"Algorithm"},
{Id:8,Rate:4,Price:160,Name:"Biology"},
{Id:9,Rate:4,Price:90,Name:"Software Engineering"},
{Id:2,Rate:5,Price:150,Name:"Geographic"},
{Id:1,Rate:5,Price:200,Name:"History"},
{Id:19,Rate:6,Price:150,Name:"C#"},
{Id:20,Rate:7,Price:250,Name:"ASP.Net"}
]
Here you can see it is sorted on Rate as well as Name.
Why am I not doing sorting with multiple keys once and for all? Because I am creating a function which sorts the array of objects and can be called multiple times.
Example:
myarray.order('Rate') // single sort
myarray.order('Rate').order('Name') // multiple sort
I can use some more parameters in my function to track if array has already been sorted.
Sample Fiddle
Assuming that you already have bookArray sorted by Rate. It can be sorted on second level using following. Can be fine tuned better for easy usability.
var arr = bookArray;
var firstSortKey = 'Rate';
var secondSortKey = 'Name';
var count = 0 ;
var firstSortValue = arr[count][firstSortKey];
for(var i=count+1; i<arr.length; i++){
if(arr[i][firstSortKey]!==firstSortValue){
var data = arr.slice(count, i);
data = data.sort(function(a, b){
return a[secondSortKey]>b[secondSortKey];
});
var argsArray = [count, i-count];
argsArray.push.apply(argsArray, data);
Array.prototype.splice.apply(arr, argsArray);
count = i;
firstSortValue = arr[i][firstSortKey];
}
}
Fiddle Demo
Change your sortBy function to accept an array of keys. Something like:
function sortBy(keys, reverse) {
var moveSmaller = reverse ? 1 : -1;
var moveLarger = reverse ? -1 : 1;
var key = keys.shift();
return function(a, b) {
if (a[key] < b[key]) {
return moveSmaller;
}
if (a[key] > b[key]) {
return moveLarger;
}
return keys.length ? (sortBy(keys, reverse))(a, b) : 0;
};
}
booksArray.sort(sortBy(['Rate', 'Name'], false))
(Untested, jsfiddle is down for me)
Just for fun: here's a generic sorter for multiple levels of field values within an Array of Objects.
Syntax
XSort().create([arrOfObjects])
.orderBy(
{key: [keyname], [descending=0/1] },
{key: [keyname], [descending=0/1]},
... );
See jsFiddle for an example using booksArray.
function XSort() {
const multiSorter = sortKeys => {
if (!sortKeys || sortKeys[0].constructor !== Object) {
throw new TypeError("Provide at least one {key: [keyname]} to sort on");
}
return function (val0, val1) {
for (let sortKey of sortKeys) {
const v0 = sortKey.key instanceof Function
? sortKey.key(val0) : val0[sortKey.key];
const v1 = sortKey.key instanceof Function
? sortKey.key(val1) : val1[sortKey.key];
const isString = v0.constructor === String || v1.constructor === String;
const compare = sortKey.descending ?
isString ? v1.toLowerCase().localeCompare(v0.toLowerCase()) : v1 - v0 :
isString ? v0.toLowerCase().localeCompare(v1.toLowerCase()) : v0 - v1;
if ( compare !== 0 ) { return compare; }
}
};
}
const Sorter = function (array) {
this.array = array;
};
Sorter.prototype = {
orderBy: function(...sortOns) {
return this.array.slice().sort(multiSorter(sortOns));
},
};
return {
create: array => new Sorter(array)
};
}
I've got a 'table' of two columns represented as an array. The first column are numbers from 1 to 20 and they are labels, the second column are the corresponding values (seconds):
my_array = [ [ 3,4,5,3,4,5,2 ],[ 12,14,16,11,12,10,20 ] ];
I need the mean (average) for each label:
my_mean_array = [ [ 2,3,4,5 ],[ 20/1, (12+11)/2, (14+12)/2, (16+10)/2 ] ];
// edit: The mean should be a float - the notion above is just for clarification.
// Also the number 'labels' should remain as numbers/integers.
My try:
var a = my_array[0];
var b = my_array[1];
m = [];
n = [];
for( var i = 0; a.length; i++){
m[ a[i] ] += b[i]; // accumulate the values in the corresponding place
n[ a[i] ] += 1; // count the occurences
}
var o = [];
var p = [];
o = m / n;
p.push(n);
p.push(o);
How about this (native JS, will not break on older browsers):
function arrayMean(ary) {
var index = {}, i, label, value, result = [[],[]];
for (i = 0; i < ary[0].length; i++) {
label = ary[0][i];
value = ary[1][i];
if (!(label in index)) {
index[label] = {sum: 0, occur: 0};
}
index[label].sum += value;
index[label].occur++;
}
for (i in index) {
if (index.hasOwnProperty(i)) {
result[0].push(parseInt(i, 10));
result[1].push(index[i].occur > 0 ? index[i].sum / index[i].occur : 0);
}
}
return result;
}
FWIW, if you want fancy I've created a few other ways to do it. They depend on external libraries and are very probably an order of magnitude slower than a native solution. But they are nicer to look at.
It could look like this, with underscore.js:
function arrayMeanUnderscore(ary) {
return _.chain(ary[0])
.zip(ary[1])
.groupBy(function (item) { return item[0]; })
.reduce(function(memo, items) {
var values = _.pluck(items, 1),
toSum = function (a, b) { return a + b; };
memo[0].push(items[0][0]);
memo[1].push(_(values).reduce(toSum) / values.length);
return memo;
}, [[], []])
.value();
}
// --------------------------------------------
arrayMeanUnderscore([[3,4,5,3,4,5,2], [12,14,16,11,12,10,20]]);
// -> [[2,3,4,5], [20,11.5,13,13]]
or like this, with the truly great linq.js (I've used v2.2):
function arrayMeanLinq(ary) {
return Enumerable.From(ary[0])
.Zip(ary[1], "[$, $$]")
.GroupBy("$[0]")
.Aggregate([[],[]], function (result, item) {
result[0].push(item.Key());
result[1].push(item.Average("$[1]"));
return result;
});
}
// --------------------------------------------
arrayMeanLinq([[3,4,5,3,4,5,2], [12,14,16,11,12,10,20]]);
// -> [[3,4,5,2], [11.5,13,13,20]]
As suspected, the "fancy" implementations are an order of magnitude slower than a native implementation: jsperf comparison.
var temp = {};
my_array[0].map(function(label, i) {
if (! temp[label])
{
temp[label] = [];
}
temp[label].push(my_array[1][i]);
});
var result = [ [], [] ];
for (var label in temp) {
result[0].push(label);
result[1].push(
temp[label].reduce(function(p, v) { return p + v }) / temp[label].length
);
}
This function do not sort the resulted array like in your result example. If you need sorting, just say me and i will add it.
function getMeanArray(my_array)
{
m = {}; //id={count,value}
for( var i = 0; i<my_array[0].length; i++){
if (m[my_array[0][i]]===undefined)
{
m[my_array[0][i]]={count:0, value:0};
}
m[ my_array[0][i] ].value += my_array[1][i]; // accumulate the values in the corresponding place
m[ my_array[0][i] ].count++; // count the occurences
}
var my_mean_array=[[],[]];
for (var id in m)
{
my_mean_array[0].push(id);
my_mean_array[1].push(m[id].count!=0?m[id].value/m[id].count:0);
}
return my_mean_array;
}