Sort Strings that have plus or minus - javascript

I am wondering if there is any way besides the very manual method of sorting the following (an example):
BBB+
BB
AA+
A-
BB-
A
B
AAA
We are trying to get the previous strings sorted as following :
AAA
AA+
A
A-
BBB+
BB
BB-
B
Is there any automatic sort that compares plus and minus putting plus first?

As I described in my comment, we can change the ratings into numbers so we can use basic sorting over complicated letter sorting.
const ratings = [
"BBB+",
"BB",
"AA+",
"A-",
"BB-",
"A",
"B",
"AAA"
];
// Each letter should be enough apart.
// Technically we can go for smaller numbers.
// But that can break if not carefull once we start using Moodys or S&P instead of Fitch.
// Since AA- is lower than AA, the - character has to be a negative number.
const weights = {
"A": 10000,
"B": 1000,
"C": 100,
"D": 10,
"+": 1,
"-": -1
};
const rating_values = ratings
.map( rating => ( {
rating,
"value": rating
.split( '' )
.map( character => weights[ character ] )
.reduce( ( a, b ) => a + b )
} ) );
const sorted_ratings = rating_values
.sort( ( a, b ) => b.value - a.value )
.map( obj => obj.rating );
console.log( sorted_ratings );

You could use three step sorting by splitting the letters from plus/minus sign.
For getting a sortable value for '+' and '-', this approach uses a weight for these characters and takes zero for not given signs.
Then
sort by the first character ascending,
sort by the all characters descending,
sort by weight ascending ('+' is smaller than '-')
var data = ['BBB+', 'BB', 'AA+', 'A-', 'BB-', 'A', 'B', 'AAA', 'AAA+', 'AAA-'];
data.sort((a, b) => {
var weight = { '+': -1, '-': 1 },
aa = a.split(/(?=[+\-])/),
bb = b.split(/(?=[+\-])/);
return aa[0][0].localeCompare(bb[0][0])
|| bb[0].localeCompare(aa[0])
|| (weight[aa[1]] || 0) - (weight[bb[1]] || 0);
});
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }

You want to do something like this perhaps:
let marks = {"A": 6, "B": 1, "+": 0.5, "-": -0.5}
customGradeSort = (mark1, mark2) => {
let value1 = mark1.split("").reduce((total, e) => {
total += marks[e];
return total;
}, 0);
let value2 = mark2.split("").reduce((total, e) => {
total += marks[e];
return total;
}, 0);
return (value2 - value1)
}
const myMarks = ["BBB+", "BB","AA+","A-","BB-","A","B","AAA"]
console.log(myMarks.sort(customGradeSort));
NOTE: take value of A such that A- > BBB+
In this case A- is 5.5 and BBB+ is 3.5.

I approached the problem by identyfying the sorting criteria. In this case they are in the following order: alphabetical, length and symbol.
And it brought me to the simple and short solution.
It also covers cases with different letters and their count:
const input = ["BBB+", "BB", "AA+", "A-", "BB-","A","B", "AAA"];
const sorted = input.sort((a,b) => {
const [ [ aStr, aSign ], [ bStr, bSign ] ] = [a,b].map(str => str.match(/[A-Z]+|[\+|\-]/gi));
const [aSignNum, bSignNum] = [aSign, bSign].map(a => a === "+" ? 1 : a === "-" ? -1 : 0);
const alphCriteria = aStr[0].localeCompare(bStr[0]);
const countCriteria = bStr.length - aStr.length;
const signCriteria = bSignNum - aSignNum;
return alphCriteria || countCriteria || signCriteria;
});
console.log(sorted);

Knowing I'm late, I will still contribute.
What I have done:
Create an object with weights for different characters
Sorted accordingly
let arr = ["A", "A-", "AA+", "AAA", "B", "BB", "BB-", "BBB+"];
let weights = {
A: 100,
B: 10,
'+': 1,
'-': -1
}
function getWeightSum(str) {
let weightSum = 0;
// Iterate all characters in string
// Accumulate total weight
for (let i = 0; i < str.length; i++) {
weightSum += weights[str[i]];
}
return weightSum;
}
function stringSort(a, b) {
let aInt = getWeightSum(a);
let bInt = getWeightSum(b);
if (aInt > bInt) return -1;
else if (aInt <= bInt) return 1;
else return 0;
}
console.log(arr.sort(stringSort));

My version is similar to the one from Nina (and I simplified by stealing that answer's localeCompare rather than my first fairly silly nested conditional operators.) But it captures a somewhat different description of each element, and uses destructuring and named values rather than array indices.
It also has a slightly different manner of handling +/-.
const compareRatings = (a, b) => {
// e.g. ['BBB', 'B', '+'] or ['BB', 'B', '']
const describe = r => r.match(/(([A-Z])\2*)([\-\+]?)/).slice(1)
const [base1, char1, ext1] = describe(a)
const [base2, char2, ext2] = describe(b)
return char1.localeCompare(char2) ||
base2.localeCompare(base1) ||
// Unicode: 43: '+', 44: ',', 45: '-'
((ext1 || ',').charCodeAt(0) - (ext2 || ',').charCodeAt(0))
}
const ratings = ['BBB+', 'BB', 'AA+', 'A-', 'BB-', 'A', 'B', 'AAA']
console.log(ratings.sort(compareRatings))
I prefer this technique to those that weight the letters because this way can extend to as many different letters as you like and to as many copies of each. It will not for instance sort "BBBBBBBBBBB" before "A", as many of those would, and it handles "XX-" just fine.

Related

Group table data into groups of N in Typescript [duplicate]

Let's say I have this array of characters I want to use:
var acceptableCharacters = ['a','b','c','d'];
I'd like to get all possible combinations of this array, say from a length of 2 to 4, so the output might look something like this
aa
ab
ac
ad
ba
bb
bc
bd
ca
cb
cc
cd
da
db
dc
dd
...And so on...
The generator should be able to meet these conditions:
Aims for all combinations between 2 lengths, or allows itself to accept any different length
Letters can repeat themself
I'm not really sure where to start, so some starter code would be nice.
edit:
var characters = ['a','b','c','d'];
var combinations = [];
for(var i=2;i<=4;i++) {
var str = "";
for(var c of characters) {
str+=c;
// 'a' spam
}
combinations.push(str);
};
console.log( combinations );
I don't know how to iterate the characters ¯_(ツ)_/¯
It looks like this:
ab
abc
abcd
The issue I see with your code is that you need a nested loop for each additional combination length, i.e. for length 2 you need a double-nested iteration, for length 3 a triple-nested iteration, and so on.
Took a stab at an implementation using recursion. It recurses down to the base case of length 1 and builds up the result set.
const characters = ["a", "b", "c", "d"];
const combinations = (arr, min = 1, max) => {
const combination = (arr, depth) => {
if (depth === 1) {
return arr;
} else {
const result = combination(arr, depth - 1).flatMap((val) =>
arr.map((char) => val + char)
);
return arr.concat(result);
}
};
return combination(arr, max).filter((val) => val.length >= min);
};
const result = combinations(characters, 2, 4);
console.log(`Combinations: ${result.length}`, result);
Here's a non-recursive version; it ended up being a bit simpler.
const characters = ["a", "b", "c", "d"];
const combinations = (arr, min = 1, max) =>
[...Array(max).keys()]
.reduce(
(result) =>
arr.concat(result.flatMap((val) => arr.map((char) => val + char))),
[]
)
.filter((val) => val.length >= min);
const result = combinations(characters, 2, 4);
console.log(`Combinations: ${result.length}`, result);

Fast anagrams checker algorithm (HackerRank)

Problem:
Given two arrays of strings, for every string in list (query), determine how many anagrams of it are in the other list (dictionary).
It should return an array of integers.
Example:
query = ["a", "nark", "bs", "hack", "stair"]
dictionary = ['hack', 'a', 'rank', 'khac', 'ackh', 'kran', 'rankhacker', 'a', 'ab', 'ba', 'stairs', 'raits']
The answer would be [2, 2, 0, 3, 1] since query[0] ('a') has 2 anagrams in dictionary: 'a' and 'a' and so on...
This was the code I came up with:
function sortArray(array) {
let answer = [];
for(let i = 0; i< array.length ; i++) {
let data = array[i].split('').sort().join('');
answer.push(data);
}
return answer;
}
function stringAnagram(dictionary, query) {
// Write your code here
let sortedDict = sortArray(dictionary);
let sortedQuery = sortArray(query);
let answer = [];
console.log(sortedDict.length);
console.log(sortedQuery.length);
sortedQuery.map(data => {
let i = 0;
sortedDict.forEach(dictData => {
if(data === dictData)
i++;
})
answer.push(i);
})
return answer;
}
However it is returning timeout error for longer test cases. Need some help optimizing it. Any suggestions? I'm trying to achieve it in JavaScript.
You may want to avoid using (expensive) Array.prototype.sort() to detect anagram and give your anagram detection algorithm as much shortcuts as possible.
So, if assume, anagrams should be the strings of the same length with the same count of the same characters, you may go something, like that:
const query = ["a", "nark", "bs", "hack", "stair"],
dictionary = ['hack', 'a', 'rank', 'khac', 'ackh', 'kran', 'rankhacker', 'a', 'ab', 'ba', 'stairs', 'raits'],
charCount = s => [...s].reduce((acc,c) =>
(acc[c]=(acc[c]||0)+1, acc), {}),
areAnagrams = (s1, s2) => {
if(s1.length != s2.length) return false
const s1CharCount = charCount(s1),
s2CharCount = charCount(s2),
result = Object
.keys(s1CharCount)
.every(char =>
s2CharCount[char] == s1CharCount[char])
return result
},
outcome = query.map(word =>
dictionary
.filter(_word => areAnagrams(word, _word))
.length
)
console.log(outcome)
Slightly more verbose way of doing it - but it works and makes sense to me - for each word in the original array, find the words in the target array that are the same length and then count those that are anagrams of the original word (an anagram is another word made up of the same letters in any order).
So the steps are -
Iterate over the firat array and for each word - filter the target array to get all words that are the same length as that word (potentialAnagrams)
Then iterate over that potentialAnagrams array and pass each word to a function that checks if all the letters and only the letters in the original word are present (in the given example - that would be [2, 2, 0, 3, 1]
Add up all anagrams of the word and pass the count to an array that is logged as the final result.
const queryArr = ["a", "nark", "bs", "hack", "stair"];
const dictionaryArr = ['hack', 'a', 'rank', 'khac', 'ackh', 'kran', 'rankhacker', 'a', 'ab', 'ba', 'stairs', 'raits'];
let anagramsArr = [];
queryArr.forEach(function(query){
let anagramsCount = 0
const potentialAnagrams = dictionaryArr.filter(el => el.length === query.length);
potentialAnagrams.forEach(function(potentialAnagram){
if(isAnagram(query,potentialAnagram)){
anagramsCount++
}
})
anagramsArr.push(anagramsCount);
})
function isAnagram(word1, word2){
let count = 0;
const word1Arr = word1.split('');
const word2Arr = word2.split('');
if( word1Arr.length !== word2Arr.length) {
return 'Invalid data - words 1 and 2 are of different lengths';
}
word1Arr.forEach(function(letter){
if(word2.indexOf(letter) !== -1) {
count++
}
})
return count === word1Arr.length
}
console.log(isAnagram('ab', 'bab')); //gives 'Invalid data - words 1 and 2 are of different lengths';
console.log(anagramsArr); //gives [2, 2, 0, 3,1];

Sorting strings in descending order in Javascript (Most efficiently)?

W3CSchools has this example:
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.sort();
fruits.reverse();
Is this the most efficient way to sort strings in descending order in Javascript?
Update
One of the answers is using localeCompare. Just curious whether if we do reverse(), will that work for all locales (Maybe this is a separate question - Just let me know in the comments)?
If you consider
obj.sort().reverse();
VS
obj.sort((a, b) => (a > b ? -1 : 1))
VS
obj.sort((a, b) => b.localeCompare(a) )
The performance winner is : obj.sort().reverse().
Testing with an array of 10.000 elements, obj.sort().reverse() is faster than obj.sort( function ) (except on chrome), and obj.sort( function ) (using localCompare).
Performance test here :
var results = [[],[],[]]
for(let i = 0; i < 100; i++){
const randomArrayGen = () => Array.from({length: 10000}, () => Math.random().toString(30));
const randomArray = randomArrayGen();
const copyArray = x => x.slice();
obj = copyArray(randomArray);
let t0 = performance.now();
obj.sort().reverse();
let t1 = performance.now();
obj = copyArray(randomArray);
let t2 = performance.now();
obj.sort((a, b) => (a > b ? -1 : 1))
let t3 = performance.now();
obj = copyArray(randomArray);
let t4 = performance.now();
obj.sort((a, b) => b.localeCompare(a))
let t5 = performance.now();
results[0].push(t1 - t0);
results[1].push(t3 - t2);
results[2].push(t5 - t4);
}
const calculateAverage = x => x.reduce((a,b) => a + b) / x.length ;
console.log("obj.sort().reverse(): " + calculateAverage(results[0]));
console.log("obj.sort((a, b) => (a > b ? -1 : 1)): " + calculateAverage(results[1]));
console.log("obj.sort((a, b) => b.localeCompare(a)): " + calculateAverage(results[2]));
Using just sort and reverse a > Z , that is wrong if you want to order lower cases and upper cases strings:
var arr = ["a","b","c","A","B","Z"];
arr.sort().reverse();
console.log(arr)//<-- [ 'c', 'b', 'a', 'Z', 'B', 'A' ] wrong!!!
English characters
var arr = ["a","b","c","A","B","Z"];
arr.sort((a,b)=>b.localeCompare(a))
console.log(arr)
Special characters using locales, in this example es (spanish)
var arr = ["a", "á", "b","c","A","Á","B","Z"];
arr.sort((a, b) => b.localeCompare(a, 'es', {sensitivity: 'base'}))
console.log(arr)
sensitivity in this case is base:
Only strings that differ in base letters compare as unequal. Examples:
a ≠ b, a = á, a = A.
The easiest way to revers the order of sorting is by swapping the operands. In ES2015 that's as easy as [b, a] = [a, b]. A full example:
function compareWithOrder(a, b, shouldReverse = false) {
if (shouldReverse) {
[b, a] = [a, b]
}
return yourComparatorFn(a, b)
}
var arr = ["a","b","c","A","B","Z"];
arr.sort((a,b)=>b.localeCompare(a))
console.log(arr)
I know this is an old question, but an interesting one. This is my solution for a non-special character's input.
var arr = ["a","b","c","A","B","Z"];
console.log(arr.sort((a,b)=> {
const lastCodeIn = b.toLowerCase().charCodeAt();
const lastCode = b.charCodeAt();
const firstCodeIn = a.toLowerCase().charCodeAt();
const firstCode = a.charCodeAt();
if(lastCodeIn - firstCodeIn === 0){
return lastCode - firstCode;
}
return lastCodeIn - firstCodeIn;
})
);//[ 'Z', 'c', 'b', 'B', 'a', 'A' ]
The reason is that ascii code for UPPER case are lower than lower case.

JavaScript: How to sort array of objects by two object properties? [duplicate]

I have a multidimensional array. The primary array is an array of
[publicationID][publication_name][ownderID][owner_name]
What I am trying to do is sort the array by owner_name and then by publication_name. I know in JavaScript you have Array.sort(), into which you can put a custom function, in my case i have:
function mysortfunction(a, b) {
var x = a[3].toLowerCase();
var y = b[3].toLowerCase();
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
}
This is fine for just sorting on the one column, namely owner_name, but how do I modify it to sort on owner_name, then publication_name?
If owner names differ, sort by them. Otherwise, use publication name for tiebreaker.
function mysortfunction(a, b) {
var o1 = a[3].toLowerCase();
var o2 = b[3].toLowerCase();
var p1 = a[1].toLowerCase();
var p2 = b[1].toLowerCase();
if (o1 < o2) return -1;
if (o1 > o2) return 1;
if (p1 < p2) return -1;
if (p1 > p2) return 1;
return 0;
}
I think what you're looking for is thenBy.js: https://github.com/Teun/thenBy.js
It allows you to use the standard Array.sort, but with firstBy().thenBy().thenBy() style.
An example can be seen here.
A good way to sort on many fields that are strings is to use toLocaleCompare and the boolean operator ||.
Something like:
// Sorting record releases by name and then by title.
releases.sort((oldRelease, newRelease) => {
const compareName = oldRelease.name.localeCompare(newRelease.name);
const compareTitle = oldRelease.title.localeCompare(newRelease.title);
return compareName || compareTitle;
})
If you wanted to sort on more fields, you could simply chain them off the return statement with more boolean operators.
Came across a need to do SQL-style mixed asc and desc object array sorts by keys.
kennebec's solution above helped me get to this:
Array.prototype.keySort = function(keys) {
keys = keys || {};
// via
// https://stackoverflow.com/questions/5223/length-of-javascript-object-ie-associative-array
var obLen = function(obj) {
var size = 0, key;
for (key in obj) {
if (obj.hasOwnProperty(key))
size++;
}
return size;
};
// avoiding using Object.keys because I guess did it have IE8 issues?
// else var obIx = function(obj, ix){ return Object.keys(obj)[ix]; } or
// whatever
var obIx = function(obj, ix) {
var size = 0, key;
for (key in obj) {
if (obj.hasOwnProperty(key)) {
if (size == ix)
return key;
size++;
}
}
return false;
};
var keySort = function(a, b, d) {
d = d !== null ? d : 1;
// a = a.toLowerCase(); // this breaks numbers
// b = b.toLowerCase();
if (a == b)
return 0;
return a > b ? 1 * d : -1 * d;
};
var KL = obLen(keys);
if (!KL)
return this.sort(keySort);
for ( var k in keys) {
// asc unless desc or skip
keys[k] =
keys[k] == 'desc' || keys[k] == -1 ? -1
: (keys[k] == 'skip' || keys[k] === 0 ? 0
: 1);
}
this.sort(function(a, b) {
var sorted = 0, ix = 0;
while (sorted === 0 && ix < KL) {
var k = obIx(keys, ix);
if (k) {
var dir = keys[k];
sorted = keySort(a[k], b[k], dir);
ix++;
}
}
return sorted;
});
return this;
};
sample usage:
var obja = [
{USER:"bob", SCORE:2000, TIME:32, AGE:16, COUNTRY:"US"},
{USER:"jane", SCORE:4000, TIME:35, AGE:16, COUNTRY:"DE"},
{USER:"tim", SCORE:1000, TIME:30, AGE:17, COUNTRY:"UK"},
{USER:"mary", SCORE:1500, TIME:31, AGE:19, COUNTRY:"PL"},
{USER:"joe", SCORE:2500, TIME:33, AGE:18, COUNTRY:"US"},
{USER:"sally", SCORE:2000, TIME:30, AGE:16, COUNTRY:"CA"},
{USER:"yuri", SCORE:3000, TIME:34, AGE:19, COUNTRY:"RU"},
{USER:"anita", SCORE:2500, TIME:32, AGE:17, COUNTRY:"LV"},
{USER:"mark", SCORE:2000, TIME:30, AGE:18, COUNTRY:"DE"},
{USER:"amy", SCORE:1500, TIME:29, AGE:19, COUNTRY:"UK"}
];
var sorto = {
SCORE:"desc",TIME:"asc", AGE:"asc"
};
obja.keySort(sorto);
yields the following:
0: { USER: jane; SCORE: 4000; TIME: 35; AGE: 16; COUNTRY: DE; }
1: { USER: yuri; SCORE: 3000; TIME: 34; AGE: 19; COUNTRY: RU; }
2: { USER: anita; SCORE: 2500; TIME: 32; AGE: 17; COUNTRY: LV; }
3: { USER: joe; SCORE: 2500; TIME: 33; AGE: 18; COUNTRY: US; }
4: { USER: sally; SCORE: 2000; TIME: 30; AGE: 16; COUNTRY: CA; }
5: { USER: mark; SCORE: 2000; TIME: 30; AGE: 18; COUNTRY: DE; }
6: { USER: bob; SCORE: 2000; TIME: 32; AGE: 16; COUNTRY: US; }
7: { USER: amy; SCORE: 1500; TIME: 29; AGE: 19; COUNTRY: UK; }
8: { USER: mary; SCORE: 1500; TIME: 31; AGE: 19; COUNTRY: PL; }
9: { USER: tim; SCORE: 1000; TIME: 30; AGE: 17; COUNTRY: UK; }
keySort: { }
(using a print function from here)
here is a jsbin example.
edit: cleaned up and posted as mksort.js on github.
This is handy for alpha sorts of all sizes.
Pass it the indexes you want to sort by, in order, as arguments.
Array.prototype.deepSortAlpha= function(){
var itm, L=arguments.length, order=arguments;
var alphaSort= function(a, b){
a= a.toLowerCase();
b= b.toLowerCase();
if(a== b) return 0;
return a> b? 1:-1;
}
if(!L) return this.sort(alphaSort);
this.sort(function(a, b){
var tem= 0, indx=0;
while(tem==0 && indx<L){
itm=order[indx];
tem= alphaSort(a[itm], b[itm]);
indx+=1;
}
return tem;
});
return this;
}
var arr= [[ "Nilesh","Karmshil"], ["Pranjal","Deka"], ["Susants","Ghosh"],
["Shiv","Shankar"], ["Javid","Ghosh"], ["Shaher","Banu"], ["Javid","Rashid"]];
arr.deepSortAlpha(1,0);
I suggest to use a built in comparer and chain the wanted sort order with logical or ||.
function customSort(a, b) {
return a[3].localeCompare(b[3]) || a[1].localeCompare(b[1]);
}
Working example:
var array = [
[0, 'Aluminium', 0, 'Francis'],
[1, 'Argon', 1, 'Ada'],
[2, 'Brom', 2, 'John'],
[3, 'Cadmium', 3, 'Marie'],
[4, 'Fluor', 3, 'Marie'],
[5, 'Gold', 1, 'Ada'],
[6, 'Kupfer', 4, 'Ines'],
[7, 'Krypton', 4, 'Joe'],
[8, 'Sauerstoff', 3, 'Marie'],
[9, 'Zink', 5, 'Max']
];
array.sort(function (a, b) {
return a[3].localeCompare(b[3]) || a[1].localeCompare(b[1]);
});
document.write('<pre>');
array.forEach(function (a) {
document.write(JSON.stringify(a) + '<br>');
});
You could concat the 2 variables together into a sortkey and use that for your comparison.
list.sort(function(a,b){
var aCat = a.var1 + a.var2;
var bCat = b.var1 + b.var2;
return (aCat > bCat ? 1 : aCat < bCat ? -1 : 0);
});
I found multisotr. This is simple, powerfull and small library for multiple sorting. I was need to sort an array of objects with dynamics sorting criteria:
const criteria = ['name', 'speciality']
const data = [
{ name: 'Mike', speciality: 'JS', age: 22 },
{ name: 'Tom', speciality: 'Java', age: 30 },
{ name: 'Mike', speciality: 'PHP', age: 40 },
{ name: 'Abby', speciality: 'Design', age: 20 },
]
const sorted = multisort(data, criteria)
console.log(sorted)
<script src="https://cdn.rawgit.com/peterkhayes/multisort/master/multisort.js"></script>
This library more mutch powerful, that was my case. Try it.
String Appending Method
You can sort by multiple values simply by appending the values into a string and comparing the strings. It is helpful to add a split key character to prevent runoff from one key to the next.
Example
const arr = [
{ a: 1, b: 'a', c: 3 },
{ a: 2, b: 'a', c: 5 },
{ a: 1, b: 'b', c: 4 },
{ a: 2, b: 'a', c: 4 }
]
function sortBy (arr, keys, splitKeyChar='~') {
return arr.sort((i1,i2) => {
const sortStr1 = keys.reduce((str, key) => str + splitKeyChar+i1[key], '')
const sortStr2 = keys.reduce((str, key) => str + splitKeyChar+i2[key], '')
return sortStr1.localeCompare(sortStr2)
})
}
console.log(sortBy(arr, ['a', 'b', 'c']))
Recursion Method
You can also use Recursion to do this. It is a bit more complex than the String Appending Method but it allows you to do ASC and DESC on the key level. I'm commenting on each section as it is a bit more complex.
There are a few commented out tests to show and verify the sorting works with a mixture of order and default order.
Example
const arr = [
{ a: 1, b: 'a', c: 3 },
{ a: 2, b: 'a', c: 5 },
{ a: 1, b: 'b', c: 4 },
{ a: 2, b: 'a', c: 4 }
]
function sortBy (arr, keys) {
return arr.sort(function sort (i1,i2, sKeys=keys) {
// Get order and key based on structure
const compareKey = (sKeys[0].key) ? sKeys[0].key : sKeys[0];
const order = sKeys[0].order || 'ASC'; // ASC || DESC
// Calculate compare value and modify based on order
let compareValue = i1[compareKey].toString().localeCompare(i2[compareKey].toString())
compareValue = (order.toUpperCase() === 'DESC') ? compareValue * -1 : compareValue
// See if the next key needs to be considered
const checkNextKey = compareValue === 0 && sKeys.length !== 1
// Return compare value
return (checkNextKey) ? sort(i1, i2, sKeys.slice(1)): compareValue;
})
}
// console.log(sortBy(arr, ['a', 'b', 'c']))
console.log(sortBy(arr, [{key:'a',order:'desc'}, 'b', 'c']))
// console.log(sortBy(arr, ['a', 'b', {key:'c',order:'desc'}]))
// console.log(sortBy(arr, ['a', {key:'b',order:'desc'}, 'c']))
// console.log(sortBy(arr, [{key:'a',order:'asc'}, {key:'b',order:'desc'}, {key:'c',order:'desc'}]))
Try this:
t.sort( (a,b)=> a[3].localeCompare(b[3]) || a[1].localeCompare(b[1]) );
let t = [
//[publicationID, publication_name, ownderID, owner_name ]
[1, 'ZBC', 3, 'John Smith'],
[2, 'FBC', 5, 'Mike Tyson'],
[3, 'ABC', 7, 'Donald Duck'],
[4, 'DBC', 1, 'Michael Jackson'],
[5, 'XYZ', 2, 'Michael Jackson'],
[6, 'BBC', 4, 'Michael Jackson'],
];
// owner_name subarrray index = 3
// publication_name subarrray index = 1
t.sort( (a,b)=> a[3].localeCompare(b[3]) || a[1].localeCompare(b[1]) );
console.log(t.join('\n'));
I assume that your data in array let t = [ [publicationID, publication_name, ownderID, owner_name ], ... ] where index of owner_name = 3 and publication_name =1.
I was working with ng-grid and needed to to multiple column sorting on an array of records returned from an API, so I came up with this nifty, dynamic multi-sort function.
First of all, ng-grid fires an "event" for "ngGridSorted" and passes this structure back, describing the sort:
sortData = {
columns: DOM Element,
directions: [], //Array of string values desc or asc. Each index relating to the same index of fields
fields: [], //Array of string values
};
So I built a function that will dynamically generate a sort function based on the sortData as shown above (Don't be scared by the scroll bar! It's only about 50 lines long! Also, I'm sorry about the slop. It prevented a horizontal scrollbar!):
function SortingFunction(sortData)
{
this.sortData = sortData;
this.sort = function(a, b)
{
var retval = 0;
if(this.sortData.fields.length)
{
var i = 0;
/*
Determine if there is a column that both entities (a and b)
have that are not exactly equal. The first one that we find
will be the column we sort on. If a valid column is not
located, then we will return 0 (equal).
*/
while( ( !a.hasOwnProperty(this.sortData.fields[i])
|| !b.hasOwnProperty(this.sortData.fields[i])
|| (a.hasOwnProperty(this.sortData.fields[i])
&& b.hasOwnProperty(this.sortData.fields[i])
&& a[this.sortData.fields[i]] === b[this.sortData.fields[i]])
) && i < this.sortData.fields.length){
i++;
}
if(i < this.sortData.fields.length)
{
/*
A valid column was located for both entities
in the SortData. Now perform the sort.
*/
if(this.sortData.directions
&& i < this.sortData.directions.length
&& this.sortData.directions[i] === 'desc')
{
if(a[this.sortData.fields[i]] > b[this.sortData.fields[i]])
retval = -1;
else if(a[this.sortData.fields[i]] < b[this.sortData.fields[i]])
retval = 1;
}
else
{
if(a[this.sortData.fields[i]] < b[this.sortData.fields[i]])
retval = -1;
else if(a[this.sortData.fields[i]] > b[this.sortData.fields[i]])
retval = 1;
}
}
}
return retval;
}.bind(this);
}
I then sort the results of my API (results) like so:
results.sort(new SortingFunction(sortData).sort);
I hope somebody else enjoys this solution as much as I do! Thanks!
I had a similar problem while displaying memory pool blocks from the output of some virtual DOM h-functions composition. Basically I faced to the same problem as sorting multi-criteria data like scoring results from players around the world.
I have noticed that multi-criteria sorting is:
- sort by the first column
- if equal, sort by the second
- if equal, sort by the third
- etc... nesting and nesting if-else
And if you don't care, you could fail quickly in a if-else nesting hell... like callback hell of promises...
What about if we write a "predicate" function to decide if which part of alternative using ? The predicate is simply :
// useful for chaining test
const decide = (test, other) => test === 0 ? other : test
Now after having written your classifying tests (byCountrySize, byAge, byGameType, byScore, byLevel...) whatever who need, you can weight your tests (1 = asc, -1 = desc, 0 = disable), put them in an array, and apply a reducing 'decide' function like this:
const multisort = (s1, s2) => {
const bcs = -1 * byCountrySize(s1, s2) // -1 = desc
const ba = 1 *byAge(s1, s2)
const bgt = 0 * byGameType(s1, s2) // 0 = doesn't matter
const bs = 1 * byScore(s1, s2)
const bl = -1 * byLevel(s1, s2) // -1 = desc
// ... other weights and criterias
// array order matters !
return [bcs, ba, bgt, bs, bl].reduce((acc, val) => decide(val, acc), 0)
}
// invoke [].sort with custom sort...
scores.sort(multisort)
And voila ! It's up to you to define your own criterias / weights / orders... but you get the idea. Hope this helps !
EDIT:
* ensure that there is a total sorting order on each column
* be aware of not having dependencies between columns orders, and no circular dependencies
if, not, sorting can be unstable !
function multiSort() {
var args =$.makeArray( arguments ),
sortOrder=1, prop='', aa='', b='';
return function (a, b) {
for (var i=0; i<args.length; i++){
if(args[i][0]==='-'){
prop=args[i].substr(1)
sortOrder=-1
}
else{sortOrder=1; prop=args[i]}
aa = a[prop].toLowerCase()
bb = b[prop].toLowerCase()
if (aa < bb) return -1 * sortOrder;
if (aa > bb) return 1 * sortOrder;
}
return 0
}
}
empArray.sort(multiSort( 'lastname','firstname')) Reverse with '-lastname'
My own library for working with ES6 iterables (blinq) allows (among other things) easy multi-level sorting
const blinq = window.blinq.blinq
// or import { blinq } from 'blinq'
// or const { blinq } = require('blinq')
const dates = [{
day: 1, month: 10, year: 2000
},
{
day: 1, month: 1, year: 2000
},
{
day: 2, month: 1, year: 2000
},
{
day: 1, month: 1, year: 1999
},
{
day: 1, month: 1, year: 2000
}
]
const sortedDates = blinq(dates)
.orderBy(x => x.year)
.thenBy(x => x.month)
.thenBy(x => x.day);
console.log(sortedDates.toArray())
// or console.log([...sortedDates])
<script src="https://cdn.jsdelivr.net/npm/blinq#2.0.2"></script>
I have just published to npm a micro-library called sort-helper (source on github). The idea is to import the helper by to create the comparison function for sort array method through the syntax items.sort(by(column, ...otherColumns)), with several way to express the columns to sort by:
By key: persons.sort(by('lastName', 'firstName')),
By selector: dates.sort(by(x => x.toISOString())),
In descending order: [3, 2, 4, 1].sort(by(desc(n => n))) → [3, 2, 1, 0],
Ignoring case: ['B', 'D', 'c', 'a'].sort(by(ignoreCase(x => x))).join('') → 'aBcD'.
It's similar to the nice thenBy mentioned in this answer but with the following differences that may be more to the taste of some:
An approach more functional than object-oriented (see thenBy fluent API),
A syntax a bit terser and still as much readable, natural almost like SQL.
Fully implemented in TypeScript, to benefit from type safety and type expressivity.
Sourced from GitHub
function sortMethodAsc(a, b) {
return a == b ? 0 : a > b ? 1 : -1;
}
function sortMethodWithDirection(direction) {
if (direction === undefined || direction == "asc") {
return sortMethodAsc;
} else {
return function(a, b) {
return -sortMethodAsc(a, b);
}
}
}
function sortMethodWithDirectionByColumn(columnName, direction){
const sortMethod = sortMethodWithDirection(direction)
return function(a, b){
return sortMethod(a[columnName], b[columnName]);
}
}
function sortMethodWithDirectionMultiColumn(sortArray) {
//sample of sortArray
// sortArray = [
// { column: "column5", direction: "asc" },
// { column: "column3", direction: "desc" }
// ]
const sortMethodsForColumn = (sortArray || []).map( item => sortMethodWithDirectionByColumn(item.column, item.direction) );
return function(a,b) {
let sorted = 0;
let index = 0;
while (sorted === 0 && index < sortMethodsForColumn.length) {
sorted = sortMethodsForColumn[index++](a,b);
}
return sorted;
}
}
//=============================================
//=============================================
//=============================================
//test
var data = [
{"CountryName":"Aruba","CountryCode":"ABW","GNI":280},{
"CountryName":"Afghanistan","CountryCode":"ABW","GNI":280},{"CountryName":"Angola","CountryCode":"AGO","GNI":280},{"CountryName":"Albania","CountryCode":"ALB","GNI":4320},
{"CountryName":"Arab World","CountryCode":"ARB","GNI":280},{"CountryName":"United Arab Emirates","CountryCode":"ARE","GNI":39130},
{"CountryName":"Argentina","CountryCode":"ARG","GNI":13030},{"CountryName":"Armenia","CountryCode":"ARM","GNI":3990},{"CountryName":"American Samoa","CountryCode":"ASM","GNI":280},
{"CountryName":"Antigua and Barbuda","CountryCode":"ATG","GNI":13810},{"CountryName":"Australia","CountryCode":"AUS","GNI":51360},
{"CountryName":"Austria","CountryCode":"AUT","GNI":45440},{"CountryName":"Azerbaijan","CountryCode":"AZE","GNI":4080},{"CountryName":"Burundi","CountryCode":"BDI","GNI":280},
{"CountryName":"Belgium","CountryCode":"BEL","GNI":41790},{"CountryName":"Benin","CountryCode":"BEN","GNI":800},{"CountryName":"Burkina Faso","CountryCode":"BFA","GNI":590},
{"CountryName":"Bangladesh","CountryCode":"BGD","GNI":1470},{"CountryName":"Bulgaria","CountryCode":"BGR","GNI":7860},{"CountryName":"Bahrain","CountryCode":"BHR","GNI":21150},
{"CountryName":"Bosnia and Herzegovina","CountryCode":"BIH","GNI":4910},{"CountryName":"Belarus","CountryCode":"BLR","GNI":5280},
{"CountryName":"Belize","CountryCode":"BLZ","GNI":4390},{"CountryName":"Bolivia","CountryCode":"BOL","GNI":3130},{"CountryName":"Brazil","CountryCode":"BRA","GNI":8600},
{"CountryName":"Barbados","CountryCode":"BRB","GNI":15270},{"CountryName":"Brunei Darussalam","CountryCode":"BRN","GNI":29600},
{"CountryName":"Bhutan","CountryCode":"BTN","GNI":2660},{"CountryName":"Botswana","CountryCode":"BWA","GNI":6730},
{"CountryName":"Central African Republic","CountryCode":"CAF","GNI":390},{"CountryName":"Canada","CountryCode":"CAN","GNI":42870},
{"CountryName":"Central Europe and the Baltics","CountryCode":"CEB","GNI":13009},{"CountryName":"Switzerland","CountryCode":"CHE","GNI":80560},
{"CountryName":"Chile","CountryCode":"CHL","GNI":13610},{"CountryName":"China","CountryCode":"CHN","GNI":8690},{"CountryName":"Cote d'Ivoire","CountryCode":"CIV","GNI":1580},
{"CountryName":"Cameroon","CountryCode":"CMR","GNI":1370},{"CountryName":"Colombia","CountryCode":"COL","GNI":5890},{"CountryName":"Comoros","CountryCode":"COM","GNI":1280},
{"CountryName":"Cabo Verde","CountryCode":"CPV","GNI":3030},{"CountryName":"Costa Rica","CountryCode":"CRI","GNI":11120},
{"CountryName":"Caribbean small states","CountryCode":"CSS","GNI":8909},{"CountryName":"Cyprus","CountryCode":"CYP","GNI":23720},
{"CountryName":"Czech Republic","CountryCode":"CZE","GNI":18160},{"CountryName":"Germany","CountryCode":"DEU","GNI":43490},
{"CountryName":"Djibouti","CountryCode":"DJI","GNI":1880},{"CountryName":"Dominica","CountryCode":"DMA","GNI":6590},{"CountryName":"Denmark","CountryCode":"DNK","GNI":55220},
{"CountryName":"Dominican Republic","CountryCode":"DOM","GNI":6630},{"CountryName":"Algeria","CountryCode":"DZA","GNI":3940},
{"CountryName":"East Asia & Pacific (excluding high income)","CountryCode":"EAP","GNI":6987},{"CountryName":"Early-demographic dividend","CountryCode":"EAR","GNI":3352},
{"CountryName":"East Asia & Pacific","CountryCode":"EAS","GNI":10171},{"CountryName":"Europe & Central Asia (excluding high income)","CountryCode":"ECA","GNI":7375},
{"CountryName":"Europe & Central Asia","CountryCode":"ECS","GNI":22656},{"CountryName":"Ecuador","CountryCode":"ECU","GNI":5920},
{"CountryName":"Euro area","CountryCode":"EMU","GNI":35645},{"CountryName":"Spain","CountryCode":"ESP","GNI":27180},{"CountryName":"Estonia","CountryCode":"EST","GNI":18190},
{"CountryName":"Ethiopia","CountryCode":"ETH","GNI":740},{"CountryName":"European Union","CountryCode":"EUU","GNI":32784},
{"CountryName":"Fragile and conflict affected situations","CountryCode":"FCS","GNI":1510},{"CountryName":"Finland","CountryCode":"FIN","GNI":44580},
{"CountryName":"Fiji","CountryCode":"FJI","GNI":4970},{"CountryName":"France","CountryCode":"FRA","GNI":37970},{"CountryName":"Gabon","CountryCode":"GAB","GNI":6650},
{"CountryName":"United Kingdom","CountryCode":"GBR","GNI":40530},{"CountryName":"Georgia","CountryCode":"GEO","GNI":3780},{"CountryName":"Ghana","CountryCode":"GHA","GNI":1880},
{"CountryName":"Guinea","CountryCode":"GIN","GNI":790},{"CountryName":"Guinea-Bissau","CountryCode":"GNB","GNI":660},
{"CountryName":"Equatorial Guinea","CountryCode":"GNQ","GNI":7050},{"CountryName":"Greece","CountryCode":"GRC","GNI":18090},
{"CountryName":"Grenada","CountryCode":"GRD","GNI":9180},{"CountryName":"Guatemala","CountryCode":"GTM","GNI":4060},{"CountryName":"Guyana","CountryCode":"GUY","GNI":4500},
{"CountryName":"High income","CountryCode":"HIC","GNI":40142},{"CountryName":"Honduras","CountryCode":"HND","GNI":2250},{"CountryName":"Heavily indebted poor countries (HIPC)","CountryCode":"HPC","GNI":904},{"CountryName":"Croatia","CountryCode":"HRV","GNI":12570},{"CountryName":"Haiti","CountryCode":"HTI","GNI":760},{"CountryName":"Hungary","CountryCode":"HUN","GNI":12870},{"CountryName":"IBRD only","CountryCode":"IBD","GNI":5745},{"CountryName":"IDA & IBRD total","CountryCode":"IBT","GNI":4620},{"CountryName":"IDA total","CountryCode":"IDA","GNI":1313},{"CountryName":"IDA blend","CountryCode":"IDB","GNI":1791},
{"CountryName":"Indonesia","CountryCode":"IDN","GNI":3540},{"CountryName":"IDA only","CountryCode":"IDX","GNI":1074},{"CountryName":"India","CountryCode":"IND","GNI":1800},{"CountryName":"Ireland","CountryCode":"IRL","GNI":55290},{"CountryName":"Iraq","CountryCode":"IRQ","GNI":4630},{"CountryName":"Iceland","CountryCode":"ISL","GNI":60830},{"CountryName":"Israel","CountryCode":"ISR","GNI":37270},{"CountryName":"Italy","CountryCode":"ITA","GNI":31020},{"CountryName":"Jamaica","CountryCode":"JAM","GNI":4760},{"CountryName":"Jordan","CountryCode":"JOR","GNI":3980},{"CountryName":"Japan","CountryCode":"JPN","GNI":38550},{"CountryName":"Kazakhstan","CountryCode":"KAZ","GNI":7970},{"CountryName":"Kenya","CountryCode":"KEN","GNI":1460},{"CountryName":"Kyrgyz Republic","CountryCode":"KGZ","GNI":1130},
{"CountryName":"Cambodia","CountryCode":"KHM","GNI":1230},{"CountryName":"Kiribati","CountryCode":"KIR","GNI":3010},{"CountryName":"St. Kitts and Nevis","CountryCode":"KNA","GNI":16240},{"CountryName":"Kuwait","CountryCode":"KWT","GNI":31430},{"CountryName":"Latin America & Caribbean (excluding high income)","CountryCode":"LAC","GNI":7470},{"CountryName":"Lao PDR","CountryCode":"LAO","GNI":2270},{"CountryName":"Lebanon","CountryCode":"LBN","GNI":8400},{"CountryName":"Liberia","CountryCode":"LBR","GNI":620},{"CountryName":"Libya","CountryCode":"LBY","GNI":5500},{"CountryName":"St. Lucia","CountryCode":"LCA","GNI":8830},{"CountryName":"Latin America & Caribbean","CountryCode":"LCN","GNI":8251},{"CountryName":"Least developed countries: UN classification","CountryCode":"LDC","GNI":1011},{"CountryName":"Low income","CountryCode":"LIC","GNI":774},{"CountryName":"Sri Lanka","CountryCode":"LKA","GNI":3850},{"CountryName":"Lower middle income","CountryCode":"LMC","GNI":2118},{"CountryName":"Low & middle income","CountryCode":"LMY","GNI":4455},{"CountryName":"Lesotho","CountryCode":"LSO","GNI":1210},{"CountryName":"Late-demographic dividend","CountryCode":"LTE","GNI":8518},{"CountryName":"Lithuania","CountryCode":"LTU","GNI":15200},{"CountryName":"Luxembourg","CountryCode":"LUX","GNI":70260},{"CountryName":"Latvia","CountryCode":"LVA","GNI":14740},{"CountryName":"Morocco","CountryCode":"MAR","GNI":2860},{"CountryName":"Moldova","CountryCode":"MDA","GNI":2200},{"CountryName":"Madagascar","CountryCode":"MDG","GNI":400},{"CountryName":"Maldives","CountryCode":"MDV","GNI":9760},
{"CountryName":"Middle East & North Africa","CountryCode":"MEA","GNI":7236},{"CountryName":"Mexico","CountryCode":"MEX","GNI":8610},{"CountryName":"Marshall Islands","CountryCode":"MHL","GNI":4840},{"CountryName":"Middle income","CountryCode":"MIC","GNI":4942},{"CountryName":"Mali","CountryCode":"MLI","GNI":770},
{"CountryName":"Malta","CountryCode":"MLT","GNI":24080},{"CountryName":"Myanmar","CountryCode":"MMR","GNI":1210},{"CountryName":"Middle East & North Africa (excluding high income)","CountryCode":"MNA","GNI":3832},{"CountryName":"Montenegro","CountryCode":"MNE","GNI":7400},{"CountryName":"Mongolia","CountryCode":"MNG","GNI":3270},{"CountryName":"Mozambique","CountryCode":"MOZ","GNI":420},{"CountryName":"Mauritania","CountryCode":"MRT","GNI":1100},{"CountryName":"Mauritius","CountryCode":"MUS","GNI":10130},{"CountryName":"Malawi","CountryCode":"MWI","GNI":320},{"CountryName":"Malaysia","CountryCode":"MYS","GNI":9650},{"CountryName":"North America","CountryCode":"NAC","GNI":56721},{"CountryName":"Namibia","CountryCode":"NAM","GNI":4570},{"CountryName":"Niger","CountryCode":"NER","GNI":360},{"CountryName":"Nigeria","CountryCode":"NGA","GNI":2100},
{"CountryName":"Nicaragua","CountryCode":"NIC","GNI":2130},{"CountryName":"Netherlands","CountryCode":"NLD","GNI":46180},{"CountryName":"Norway","CountryCode":"NOR","GNI":75990},{"CountryName":"Nepal","CountryCode":"NPL","GNI":800},{"CountryName":"Nauru","CountryCode":"NRU","GNI":10220},{"CountryName":"New Zealand","CountryCode":"NZL","GNI":38970},{"CountryName":"OECD members","CountryCode":"OED","GNI":37273},{"CountryName":"Oman","CountryCode":"OMN","GNI":14440},{"CountryName":"Other small states","CountryCode":"OSS","GNI":12199},{"CountryName":"Pakistan","CountryCode":"PAK","GNI":1580},{"CountryName":"Panama","CountryCode":"PAN","GNI":13280},{"CountryName":"Peru","CountryCode":"PER","GNI":5960},{"CountryName":"Philippines","CountryCode":"PHL","GNI":3660},{"CountryName":"Palau","CountryCode":"PLW","GNI":12700},{"CountryName":"Papua New Guinea","CountryCode":"PNG","GNI":2340},{"CountryName":"Poland","CountryCode":"POL","GNI":12730},{"CountryName":"Pre-demographic dividend","CountryCode":"PRE","GNI":1379},{"CountryName":"Portugal","CountryCode":"PRT","GNI":19820},{"CountryName":"Paraguay","CountryCode":"PRY","GNI":5470},{"CountryName":"West Bank and Gaza","CountryCode":"PSE","GNI":3180},{"CountryName":"Pacific island small states","CountryCode":"PSS","GNI":3793},{"CountryName":"Post-demographic dividend","CountryCode":"PST","GNI":41609},{"CountryName":"Qatar","CountryCode":"QAT","GNI":60510},{"CountryName":"Romania","CountryCode":"ROU","GNI":10000},{"CountryName":"Russian Federation","CountryCode":"RUS","GNI":9230},{"CountryName":"Rwanda","CountryCode":"RWA","GNI":720},{"CountryName":"South Asia","CountryCode":"SAS","GNI":1729},{"CountryName":"Saudi Arabia","CountryCode":"SAU","GNI":20090},{"CountryName":"Sudan","CountryCode":"SDN","GNI":2380},{"CountryName":"Senegal","CountryCode":"SEN","GNI":1240},{"CountryName":"Singapore","CountryCode":"SGP","GNI":54530},{"CountryName":"Solomon Islands","CountryCode":"SLB","GNI":1920},{"CountryName":"Sierra Leone","CountryCode":"SLE","GNI":510},{"CountryName":"El Salvador","CountryCode":"SLV","GNI":3560},{"CountryName":"Serbia","CountryCode":"SRB","GNI":5180},{"CountryName":"Sub-Saharan Africa (excluding high income)","CountryCode":"SSA","GNI":1485},{"CountryName":"Sub-Saharan Africa","CountryCode":"SSF","GNI":1486},{"CountryName":"Small states","CountryCode":"SST","GNI":11099},{"CountryName":"Sao Tome and Principe","CountryCode":"STP","GNI":1770},{"CountryName":"Suriname","CountryCode":"SUR","GNI":5150},{"CountryName":"Slovak Republic","CountryCode":"SVK","GNI":16610},{"CountryName":"Slovenia","CountryCode":"SVN","GNI":22000},{"CountryName":"Sweden","CountryCode":"SWE","GNI":52590},{"CountryName":"Eswatini","CountryCode":"SWZ","GNI":2950},{"CountryName":"Seychelles","CountryCode":"SYC","GNI":14170},{"CountryName":"Chad","CountryCode":"TCD","GNI":640},{"CountryName":"East Asia & Pacific (IDA & IBRD countries)","CountryCode":"TEA","GNI":7061},
{"CountryName":"Europe & Central Asia (IDA & IBRD countries)","CountryCode":"TEC","GNI":7866},{"CountryName":"Togo","CountryCode":"TGO","GNI":610},{"CountryName":"Thailand","CountryCode":"THA","GNI":5950},{"CountryName":"Tajikistan","CountryCode":"TJK","GNI":990},{"CountryName":"Turkmenistan","CountryCode":"TKM","GNI":6380},{"CountryName":"Latin America & the Caribbean (IDA & IBRD countries)","CountryCode":"TLA","GNI":8179},{"CountryName":"Timor-Leste","CountryCode":"TLS","GNI":1790},{"CountryName":"Middle East & North Africa (IDA & IBRD countries)","CountryCode":"TMN","GNI":3839},{"CountryName":"Tonga","CountryCode":"TON","GNI":4010},{"CountryName":"South Asia (IDA & IBRD)","CountryCode":"TSA","GNI":1729},
{"CountryName":"Sub-Saharan Africa (IDA & IBRD countries)","CountryCode":"TSS","GNI":1486},{"CountryName":"Trinidad and Tobago","CountryCode":"TTO","GNI":15340},{"CountryName":"Tunisia","CountryCode":"TUN","GNI":3490},{"CountryName":"Turkey","CountryCode":"TUR","GNI":10940},{"CountryName":"Tuvalu","CountryCode":"TUV","GNI":4970},{"CountryName":"Tanzania","CountryCode":"TZA","GNI":910},{"CountryName":"Uganda","CountryCode":"UGA","GNI":600},{"CountryName":"Ukraine","CountryCode":"UKR","GNI":2390},{"CountryName":"Upper middle income","CountryCode":"UMC","GNI":8197},{"CountryName":"Uruguay","CountryCode":"URY","GNI":15250},{"CountryName":"United States","CountryCode":"USA","GNI":58270},{"CountryName":"Uzbekistan","CountryCode":"UZB","GNI":2000},{"CountryName":"St. Vincent and the Grenadines","CountryCode":"VCT","GNI":7390},{"CountryName":"Vietnam","CountryCode":"VNM","GNI":2160},{"CountryName":"Vanuatu","CountryCode":"VUT","GNI":2920},{"CountryName":"World","CountryCode":"WLD","GNI":10371},{"CountryName":"Samoa","CountryCode":"WSM","GNI":4090},{"CountryName":"Kosovo","CountryCode":"XKX","GNI":3900},
{"CountryName":"South Africa","CountryCode":"ZAF","GNI":5430},{"CountryName":"Zambia","CountryCode":"ZMB","GNI":1290},{"CountryName":"Zimbabwe","CountryCode":"ZWE","GNI":1170},
{"CountryName":"Zimbabwe","CountryCode":"ZWE","GNI":1171}];
const sortMethod = sortMethodWithDirectionMultiColumn(
[
{ column: "GNI", direction: "asc" },
{ column: "CountryCode", direction: "desc" }
]
);
let sortedData = data.sort(sortMethod);
console.log("sorted by: 1)column:GNI-asc, 2)column:CountryCode-desc")
console.table(sortedData);
console.log(sortedData);
I need this for a small project I'm working on, so performance is not a priority.
I have two arrays, main array I want to be sorted, and array of sorting rules. I loop that rules array inside sorting callback function, and try to exit that loop as soon as possible.
I use multiplier in order to convert -1 to 1 depending on weather I'm sorting a property in ascending or descending order.
let array = [
{fullName: 'Michael Schumacher', sport: 'Formula 1'},
{fullName: 'Michael Jordan', sport: 'Basketball'},
{fullName: 'Damon Hill', sport: 'Formula 1'},
{fullName: 'Kobe Bryant', sport: 'Basketball'},
{fullName: 'Lebron James', sport: 'Basketball'},
{fullName: 'Lewis Hamilton', sport: 'Formula 1'},
];
const sortArray = (array, options) => {
if (!Array.isArray(options)) {
options = [{ key: options, order: 'asc' }];
}
options.forEach(item => {
item.multiplier = item.order != 'desc' ? -1 : 1;
});
return array.sort((firstItem, secondItem) => {
for (item of options) {
const { key, multiplier } = item;
const firstValue = firstItem[key];
const secondValue = secondItem[key];
if (firstValue != secondValue) {
return multiplier * (firstValue < secondValue ? 1 : -1);
}
}
return 0;
});
}
console.log('Original array');
console.log([...array]);
sortArray(array, 'sport');
console.log('Sorted by sport only (ascending, implicit, keeping the same order of athletes)');
console.log([...array]);
sortArray(array, [{key: 'sport'}, {key: 'fullName', order: 'desc'}]);
console.log('Sorted by sport (ascending, implicit), and by fullName (descending)');
console.log(array);
To simplify the understanding
The sort method compares numbers, if below 0, it sorts it to the let, if above zero it sorts it to the right.
So to add multi level sorting, check if the match === 0, then further sort it.
See example below
['a/b/c', 'a long piece of text/b', 'apple/b'].sort((a, b) => {
const asc = a.split('/').length - b.split('/').length
return asc
})
// outputs ['a long piece of text/b', 'apple/b', 'a/b/c']
['a/b/c', 'a long piece of text/b', 'apple/b'].sort((a, b) => {
const asc = a.split('/').length - b.split('/').length
return asc === 0 ? a.length - b.length : asc
})
// outputs: 'apple/b', 'a long piece of text/b', 'a/b/c'
I see a lot of complicated solutions, so I'll paste here what I'm using:
assignedIssues.sort((a, b) => {
let order = sortByText(a.assignee?.username, b.assignee?.username)
if (order === 0) order = sort(a.labels, b.labels, statusLabels)
if (order === 0) order = sort(a.labels, b.labels, priorityLabels)
if (order === 0) order = sortByText(a.web_url, b.web_url)
return order
})
I think that this is much more readable, let you implement any custom sorting function for each level, without calling all unnecessarily.
Assuming you want to sort by multiple indexes, and assuming that you don't know the type of each field (string, number, or null).
You can create a function to sort with as many indexes as you like.
const compareWithType = (a, b) => {
if (typeof a === 'string') return a.localeCompare(b);
if (typeof a === 'number') return a - b;
return (!!a) - (!!b); // to sort non-string non-number falsy or null values, modify as you like.
}
const compareWithIndexes = (...indexes) => {
return (a, b) => {
for (let i in indexes) {
let diff = 0;
while (!diff) {
compareWithType(a[i], b[i]);
}
return diff;
}
}
}
[[1, 2, 3, 4, 5], [0, 2, 3, 4, 6]].sort(compareWithIndexes(2, 3, 4));
// compares (3 - 3) then (4 - 4) then (5 - 6)
Despite a lot of complicated answers here, I still like the basic way to do it
var arr = [
[3, 'pub2', 1, 'ownA'],
[1, 'pub1', 2, 'ownA'],
[2, 'pub1', 3, 'ownC']
];
// sorting priority is bottom to top, in this case owner name then publication name
// sort publication name
arr.sort((a,b) => a[1].localeCompare(b[1]));
// sort owner name
arr.sort((a,b) => a[3].localeCompare(b[3]));
console.log(arr);

Counting frequency of characters in a string using JavaScript [duplicate]

This question already has answers here:
Counting the occurrences / frequency of array elements
(39 answers)
Closed 4 months ago.
I need to write some kind of loop that can count the frequency of each letter in a string.
For example: "aabsssd"
output: a:2, b:1, s:3, d:1
Also want to map same character as property name in object. Any good idea how to do this?
I am not sure how to do it.
This is where I am so far:
var arr = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
function counter(x) {
var count = 0,
temp = [];
x = x.split('');
console.log(x);
for (var i = 0, len = x.length; i < len; i++) {
if (x[i] == "a") {
count++;
}
}
return count;
}
var a = "aabbddd";
console.log(counter(a));
Here you go:
function getFrequency(string) {
var freq = {};
for (var i=0; i<string.length;i++) {
var character = string.charAt(i);
if (freq[character]) {
freq[character]++;
} else {
freq[character] = 1;
}
}
return freq;
};
some ES6 syntax with reduce:
let counter = str => {
return str.split('').reduce((total, letter) => {
total[letter] ? total[letter]++ : total[letter] = 1;
return total;
}, {});
};
counter("aabsssd"); // => { a: 2, b: 1, s: 3, d: 1 }
Another solution:
function count (string) {
var count = {};
string.split('').forEach(function(s) {
count[s] ? count[s]++ : count[s] = 1;
});
return count;
}
With some ES6 features and short-circuiting:
const counter = s => [...s].reduce((a, c) => (a[c] = ++a[c] || 1) && a, {})
console.log(
counter("hello") // {h: 1, e: 1, l: 2, o: 1}
)
Here's another way:
const freqMap = s => [...s].reduce((freq,c) => {freq[c] = -~freq[c]; return freq} ,{})
Or, if you prefer a "for" loop:
function freqMap(s) {
freq={};
for (let c of s)
freq[c]=-~freq[c];
return freq;
}
e.g. freqMap("MaMaMia") returns Object{M : 3, a : 3, i : 1}
This method leverages the fact that in javascript, bitwise not on "undefined" gives -1, (whereas "undefined+1" gives NaN).
So, -~undefined is 1, -~1 is 2, -~2 is 3 etc.
We can thus iterate over the characters of the string, and simply increment freq[c] without any "if". The first time we encounter a character c, freq[c] will be undefined, so we set it to -~freq[c] which is 1. If we subsequently encounter c again, we again set freq[c] to -~freq[c], which will now be 2, etc.
Simple, elegant, concise.
More declarative way to get a word histogram will be to utilise reduce to iterate through letters and come up with a new object that contains letters as keys and frequencies as values.
function getFrequency(str) {
return str.split('').reduce( (prev, curr) => {
prev[curr] = prev[curr] ? prev[curr] + 1 : 1;
return prev;
}, {});
};
console.log(getFrequency('test')); // => {t: 2, e: 1, s: 1}
a leaner, functional solution:
using ES6 Arrows && Logical Operators:
const buildFreqDict = string =>
string.split('').reduce((freqDict, char) => {
freqDict[char] = (freqDict[char] || 0) + 1;
return freqDict;
}, {})
console.log(buildFreqDict("banana"))
Explained
split string into array of characters.
and then feed it into a reduce method (using method.chaining()).
if char is already logged in countDict then add 1 to it.
or if character not found in countDict then set it to 1.
return new values back up to reduce's accumulator object
NB: don't forget about including the third argument of .reduce(): in this case it is a {} (object literal) that serves to initialize the freqDict object.
for more info see Counting instances of values in an object half way down the page here: MDN Reduce
and for more info about using logical operators please see here: MDN Logical Operators
An easy way. In addition, its get you an alphabetically sorted list. It loops throught an arrray and evaluate if the character is already in the object: if false, the character is added to the object, if true, its frequency increase a unit.
const text= "Lorem ipsum dolor sit amet consectetur adipiscing"
const textAsArray = text.split('').sort()
let charactersList = {}
for (char of textAsArray) {
if (!charactersList[char]) {
charactersList[char]=1;
}
else {
charactersList[char]++
}
}
console.log(charactersList)
I have reviewed and I think that this adapts very well to the need they pose. I would like it to be in a single line but I don't know how to generate the object dynamically.
const uniqueCount=(arr)=>{
let rs ={};
arr.sort().join("").match(/(.)(\1*)/g).map(i=>rs[i[0]]=i.length);
return rs;
};
console.log(uniqueCount(["a","b","c","d","d","e","a","b","c","f","g","h","h","h","e","a"]));
//{ a: 3, b: 2, c: 2, d: 2, e: 2, f: 1, g: 1, h: 3 }
I find it very successful to use .match() and regex /(.)(\1*)/g as explained above.
If it is just a string, you just need to add a .split("") before and that's it.
One more version with sorting by alphabetically. This function works for both.
Frequency of characters by alphabetically sorted
Frequency of characters in order of occurrence
Caveat: Only works if whole string is in lowercase
function freqWithAlphabetTable(str, doNeedToSort) {
let cnt = new Array(26).fill(0), firstLowerCase = 97, output = {}
for (let i = 0; i < str.length; i++)
cnt[str[i].charCodeAt(0) - firstLowerCase]++ // filling the array with count at it's index
if (doNeedToSort) {
for (let i = 0; i < cnt.length; i++) {
if (cnt[i] !== 0)
output[String.fromCharCode(firstLowerCase)] = cnt[i]
firstLowerCase++;
}
} else {
for (let i = 0; i < str.length; i++) {
let letterIndexVal = cnt[str[i].charCodeAt(0) - firstLowerCase];
if (letterIndexVal != 0 ) {
output[str[i]] = letterIndexVal
letterIndexVal = 0 // replacing it with zero to avoid repetition
}
}
}
console.log(output);
return output;
}
for(i = strlen(string)var string = 'aabsssd';
var chars = new Array();
for(var i = 0; i < string.length; i++){
var char = string.charAt(i);
if(chars[char] == undefined){
chars[char] = 0;
}
chars[char]++;
}
console.log(chars);
Here's another option using underscore.js:
function charCount(str) {
return _(str.split('')).countBy(function(char) {
return char.toLowerCase();
});
}
charCount('aaabbbbdd') outputs Object {a: 3, b: 4, d: 2}
const recorrences = ['a', 'b', 'c', 'a', 'b','a']
.map(i => !!~i.indexOf('a'))
.filter(i => i)
.length;
console.log(`recorrences ${recorrences}`)
//recorrences 3
// Count frequency of characters in a string
// input: 'Hello, I'm Paul!'
// result: {
// H: 1,
// E: 1,
// L: 3,
// ... and so on ...
// }
const countChars = (string) => {
let charStats = {};
string = string.replace(' ', '').toUpperCase().split('');
string.forEach((char) => {
if (charStats[char]) {
charStats[char]++;
} else {
charStats[char] = 1;
}
});
return charStats;
};
Another Solution
function maxChar(str) {
const charMap = {};
let max = 0;
let maxChar = '';
for(let char of str){
if(charMap[char]){
charMap[char]++;
}else{
charMap[char] = 1;
}
}
for(let char in charMap){
if(charMap[char] > max){
max = charMap[char];
maxChar = char;
}
}
return maxChar;
}
===>
maxChar('355385')
"5"
var str = 'abcccdddd';
function maxCharCount(target) {
const chars = {};
let maxChar = '';
let maxValue = 1;
for (let char of target) {
chars[char] = chars[char] + 1 || 1;
}
return chars;
}
console.log(maxCharCount(str));
The same solution but refactored. So cool how we can solve this problem with so many different answers :)
function getFrequency(string) {
var freq = {};
for (let character in string) {
let char = string[character];
(freq[char]) ? freq[char]++ : freq[char] = 1
}
return freq;
};
You can use this. Just pass the string and it will return object with all the character frequency.
function buildCharMap(string) {
const charMap = {};
string.replace(/[^\w]/g, '').toLowerCase();
for (let char of string) {
charMap[char] = charMap[char] + 1 || 1;
}
return charMap;
}
[...str].map( char => map.get(char) ? map.set( char, map.get(char) + 1) : map.set(char,1) )
cheat code to count frequency of a char in a string is
let target = "e";
let string = " i want to see that person that came in here last";
let frequency = string.split(target).length - 1;
or all in one line
console.log(string.split("e").length - 1)
Everyone using split and reduce are over-complicating things.
string is an iterator so you can use a for/of loop to go over each letter - there's no need to split it into an array so reduce can use it. reduce is very useful for lots of things but it often seems like: "when all you have is a hammer everything looks like a nail". I think its used unnecessarily in many places.
Anyway...
Create a new object.
Loop over the string.
If there is no key in the object that corresponds to the current letter, add it and set it it to zero.
Increment it.
function counter(str) {
// Create an object
const obj = {};
// Loop through the string
for (const letter of str) {
// If the object doesn't have a `letter`
// property create one and set it to 0;
obj[letter] ??= 0;
// Increment the value
++obj[letter];
}
// Finally return the object
return obj;
}
const str = 'aabbddd';
console.log(counter(str));
Additional documentation
for/of
Logical nullish assignment

Categories