I have an array of strings I need to sort in JavaScript, but in a case-insensitive way. How to perform this?
In (almost :) a one-liner
["Foo", "bar"].sort(function (a, b) {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
Which results in
[ 'bar', 'Foo' ]
While
["Foo", "bar"].sort();
results in
[ 'Foo', 'bar' ]
It is time to revisit this old question.
You should not use solutions relying on toLowerCase. They are inefficient and simply don't work in some languages (Turkish for instance). Prefer this:
['Foo', 'bar'].sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}))
Check the documentation for browser compatibility and all there is to know about the sensitivity option.
myArray.sort(
function(a, b) {
if (a.toLowerCase() < b.toLowerCase()) return -1;
if (a.toLowerCase() > b.toLowerCase()) return 1;
return 0;
}
);
EDIT:
Please note that I originally wrote this to illustrate the technique rather than having performance in mind. Please also refer to answer #Ivan Krechetov for a more compact solution.
ES6 version:
["Foo", "bar"].sort(Intl.Collator().compare)
Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator/compare
arr.sort(function(a,b) {
a = a.toLowerCase();
b = b.toLowerCase();
if (a == b) return 0;
if (a > b) return 1;
return -1;
});
You can also use the new Intl.Collator().compare, per MDN it's more efficient when sorting arrays. The downside is that it's not supported by older browsers. MDN states that it's not supported at all in Safari. Need to verify it, since it states that Intl.Collator is supported.
When comparing large numbers of strings, such as in sorting large arrays, it is better to create an Intl.Collator object and use the function provided by its compare property
["Foo", "bar"].sort(Intl.Collator().compare); //["bar", "Foo"]
If you want to guarantee the same order regardless of the order of elements in the input array, here is a stable sorting:
myArray.sort(function(a, b) {
/* Storing case insensitive comparison */
var comparison = a.toLowerCase().localeCompare(b.toLowerCase());
/* If strings are equal in case insensitive comparison */
if (comparison === 0) {
/* Return case sensitive comparison instead */
return a.localeCompare(b);
}
/* Otherwise return result */
return comparison;
});
Normalize the case in the .sort() with .toLowerCase().
You can also use the Elvis operator:
arr = ['Bob', 'charley', 'fudge', 'Fudge', 'biscuit'];
arr.sort(function(s1, s2){
var l=s1.toLowerCase(), m=s2.toLowerCase();
return l===m?0:l>m?1:-1;
});
console.log(arr);
Gives:
biscuit,Bob,charley,fudge,Fudge
The localeCompare method is probably fine though...
Note: The Elvis operator is a short form 'ternary operator' for if then else, usually with assignment.
If you look at the ?: sideways, it looks like Elvis...
i.e. instead of:
if (y) {
x = 1;
} else {
x = 2;
}
you can use:
x = y?1:2;
i.e. when y is true, then return 1 (for assignment to x), otherwise return 2 (for assignment to x).
The other answers assume that the array contains strings. My method is better, because it will work even if the array contains null, undefined, or other non-strings.
var notdefined;
var myarray = ['a', 'c', null, notdefined, 'nulk', 'BYE', 'nulm'];
myarray.sort(ignoreCase);
alert(JSON.stringify(myarray)); // show the result
function ignoreCase(a,b) {
return (''+a).toUpperCase() < (''+b).toUpperCase() ? -1 : 1;
}
The null will be sorted between 'nulk' and 'nulm'. But the undefined will be always sorted last.
arr.sort(function(a,b) {
a = a.toLowerCase();
b = b.toLowerCase();
if( a == b) return 0;
if( a > b) return 1;
return -1;
});
In above function, if we just compare when lower case two value a and b, we will not have the pretty result.
Example, if array is [A, a, B, b, c, C, D, d, e, E] and we use the above function, we have exactly that array. It's not changed anything.
To have the result is [A, a, B, b, C, c, D, d, E, e], we should compare again when two lower case value is equal:
function caseInsensitiveComparator(valueA, valueB) {
var valueALowerCase = valueA.toLowerCase();
var valueBLowerCase = valueB.toLowerCase();
if (valueALowerCase < valueBLowerCase) {
return -1;
} else if (valueALowerCase > valueBLowerCase) {
return 1;
} else { //valueALowerCase === valueBLowerCase
if (valueA < valueB) {
return -1;
} else if (valueA > valueB) {
return 1;
} else {
return 0;
}
}
}
In support of the accepted answer I would like to add that the function below seems to change the values in the original array to be sorted so that not only will it sort lower case but upper case values will also be changed to lower case. This is a problem for me because even though I wish to see Mary next to mary, I do not wish that the case of the first value Mary be changed to lower case.
myArray.sort(
function(a, b) {
if (a.toLowerCase() < b.toLowerCase()) return -1;
if (a.toLowerCase() > b.toLowerCase()) return 1;
return 0;
}
);
In my experiments, the following function from the accepted answer sorts correctly but does not change the values.
["Foo", "bar"].sort(function (a, b) {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
This may help if you have struggled to understand:
var array = ["sort", "Me", "alphabetically", "But", "Ignore", "case"];
console.log('Unordered array ---', array, '------------');
array.sort(function(a,b) {
a = a.toLowerCase();
b = b.toLowerCase();
console.log("Compare '" + a + "' and '" + b + "'");
if( a == b) {
console.log('Comparison result, 0 --- leave as is ');
return 0;
}
if( a > b) {
console.log('Comparison result, 1 --- move '+b+' to before '+a+' ');
return 1;
}
console.log('Comparison result, -1 --- move '+a+' to before '+b+' ');
return -1;
});
console.log('Ordered array ---', array, '------------');
// return logic
/***
If compareFunction(a, b) is less than 0, sort a to a lower index than b, i.e. a comes first.
If compareFunction(a, b) returns 0, leave a and b unchanged with respect to each other, but sorted with respect to all different elements. Note: the ECMAscript standard does not guarantee this behaviour, and thus not all browsers (e.g. Mozilla versions dating back to at least 2003) respect this.
If compareFunction(a, b) is greater than 0, sort b to a lower index than a.
***/
http://jsfiddle.net/ianjamieson/wmxn2ram/1/
I wrapped the top answer in a polyfill so I can call .sortIgnoreCase() on string arrays
// Array.sortIgnoreCase() polyfill
if (!Array.prototype.sortIgnoreCase) {
Array.prototype.sortIgnoreCase = function () {
return this.sort(function (a, b) {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
};
}
Wrap your strings in / /i. This is an easy way to use regex to ignore casing
Related
I have the latest Firefox and Chrome running on Windows 10. I believe the same issue exists on Mac.
When I have a compare function can someone explain to me why when I console.log a and b, the results in Chrome are the reverse of the results in Firefox. Oddly, though, the sorted result is the same which doesn't make any sense:
var arr = [3, 1, 2];
var sorted_arr = arr.sort(function(a, b) {
console.log("a, b = ", a, b);
return a - b;
});
console.log(sorted_arr);
The first line of the console.log in Chrome is displayed as a, b = 1 3 whereas in Firefox it is a, b = 3 1.
Browsers are allowed to implement sort however they want, and you should not rely on any particular order of the calls to the comparison function.
The answer on this question might help: How does JavaScript's sort (compareFunction) work? [duplicate]
Basically, browsers have no strict way to implement sort like the other answer here stated. This longer explanation might make it more clear though, as it's a bit more in depth.
The HOST (Browser) when it comes to ECMAScript standard does not follow the policy of "implementation defined". This means that the behavior of Array.prototype.sort method may be different between engine implementations. Please refer to official documentation under these links:
https://tc39.es/ecma262/#sec-array.prototype.sort
https://tc39.es/ecma262/#implementation-defined
If you want o achieve a consistent behavior of sorting between browsers that is not obvious you should map the array to an indexed object. In this case both Chrome and Firefox result in same iteration over indexes and values.
var arr = [3,1,2];
var sorted_arr = arr.sort(function(a,b) {
console.log("a, b = ", a, b);
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
// a must be equal to b
return 0;
});
console.log("sorted_arr: ", sorted_arr)
var mapped = arr.map(function(el, i) {
return { index: i, value: el.toString().toLowerCase() };
})
mapped.sort(function(a, b) {
console.log("map: ", a, b)
if (a.value > b.value) {
return 1;
}
if (a.value < b.value) {
return -1;
}
return 0;
});
// container for the resulting order
var result = mapped.map(function(el){
return arr[el.index];
});
console.log("result: ", result)
https://jsfiddle.net/cw65b4t8/
Unusual question here and maybe I'm approaching this issue incorrectly -
I'd like to do a comparison between some numeric values in Javascript, but I have the comparators stored in a database (in a VARCHAR field), where specific criteria are stored. (Comparators being <=, <, ==, >, >=, etc).
Is there a way to evaluate the string returned as a comparator in JS?
Thanks
Yes, you could use an object with the comparators as key and return a function for making the comparison.
var comparison = {
'<=': function (a, b) { return a <= b; },
'<': function (a, b) { return a < b; },
'==': function (a, b) { return a == b; },
'>': function (a, b) { return a > b; },
'>=': function (a, b) { return a >= b; },
default: function () { return false; }
}
Usage:
(comparison[comp] || comparison.default)(value1, value2);
There is, but unless you're using it with completely trusted data, don't use it.
Instead, implement the comparisons:
function compare(operator, operand1, operand2) {
switch (operator) {
case "<=":
return operand1 <= operand2;
case ">=":
return operand1 >= operand2;
// ...and so on...
}
}
But yes, if you fully trust the data, you can use eval:
function compare(operator, operand1, operand2) {
return eval("operand1 " + operator + " operand2");
}
(That looks like pseudo-code, but it isn't; eval evaluates the code string you give it in the context where you call it, so it has access to the operand1 and operand2 arguments.)
The "trust" part there is really, really important, because eval allows executing any script code. If you're allowing Bob to provide data that will eventually be evaluated in a browser by Alice, don't use eval. It's a major security risk to Alice.
You can use eval function
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
var a = 10;
var b = 20;
var comparator = '<=';
var result = eval(a + comparator + b)
// true
Better approach would be to write your own compare method and keep results/codes on DB something like
function compare(a, b):
if( a < b ) return -2;
else if (a <= b) return -1;
else if ( a == b) return 0;
//...
and then keep values of 0, -1, -2 on db
Now, if you want to apply some comparator from your DB you just call compare on your string and check whether the result is the same as code from DB or not (and it will give you True or False)
The only problem here is that you need to fire all comparisions on dataset but it is completely safe and also will handle some inappropriate operators (that cannot be applied for a strings) - the only requirement is that code on DB must be a key
You can use jquery ajax call to retrieve the string operator from server side code and then evaluate the string for comparison in javascript code.
<script>
$(document).ready(function(){
$.ajax({url: "retrieveOperator.php", success: function(result){
switch(String(result)) {
case "==":
code block for == operator
break;
case ">=":
code block for >= operator
break;
default:
default code block
}
});
});
</script>
I have the following response I get from my server:
[
{"key":{"name":"1","kind":"a"},
{"key":{"name":"1","kind":"ap"},
{"key":{"name":"5","kind":"ap"}
]
The responses come in a different order every time as the server does not guarantee order. Thus I need to sort the response like this after I receive it:
First sort so the smallest KIND is first so 'a' should come before 'b'. I then need to make it so name of the username is the first ordered within the 'a'.
var username = '5';
var response = [
{"key":{"name":"1","kind":"a"},
{"key":{"name":"1","kind":"ap"},
{"key":{"name":"5","kind":"ap"}
];
response = entities.sort(function (a, b) {
if (a.key.kind != b.key.kind){ return a.key.kind < b.key.kind}
else if(a.key.name == username){ return a.key.name < b.key.name }
else return a.key.name > b.key.name;
});
This is the code I use to sort, but it does not work. It sorts the KIND correctly, but then when it needs to sort by NAME (username should come before other names) it does not work.
The actual result I get is equal to this:
[
{"key":{"name":"1","kind":"ap"},
{"key":{"name":"5","kind":"ap"},
{"key":{"name":"1","kind":"a"}
]
But the result I want is this:
[
{"key":{"name":"5","kind":"ap"},
{"key":{"name":"1","kind":"ap"},
{"key":{"name":"1","kind":"a"}
]
As you can see my username is equal to 5, so {"key":{"name":"5","kind":"ap"} should come before {"key":{"name":"1","kind":"ap"} .
return a.key.name < b.key.name is not saying that a < b.
They actually will be compared.
Try to replace it with return -1; to say to comparator:
a is lesser than b in that case.
When you use .sort() methods, you have to pass a compare function which will say who should comes first between two elements.
When you are comparing numbers, you can simply do :
function compare(a, b) {
return a - b;
}
this will sort an array of numbers ascending.
However, when you are comparing string, you have to define some ordering criterion, in order to tell which element should be comes first.
If compare(a, b) is less than 0, sort a to a lower index than b, so a comes first.
If compare(a, b) is greater than 0, sort b to a lower index than a, so b comes first.
If compare(a, b) is equal to 0, there is no change
So will get something like :
function compare(a, b) {
if (a is lower than b by your criteria) {
return -1;
}
if (a is greater than b by your criteria) {
return 1;
}
return 0;
}
In your case, you can write function generator that takes the property of the object to sort, and a custom sort function. This is useful when the function needs to be applied in more than one situation.
const username = '5';
const response = [
{"key":{"name":"1","kind":"a"}},
{"key":{"name":"1","kind":"ap"}},
{"key":{"name":"5","kind":"ap"}}
];
//Retrieve the value at 'props' into 'obj'
function retrieve(props, obj) {
return props.reduce((result, current) => {
result = result[current];
return result;
}, obj);
}
//Custom sort function for Kind properties
function sortKind(a, b) {
return a < b ? -1 : (a > b) ? 1 : 0;
}
//Custom sort function for name properties
function sortName(a, b) {
return a === username ? -1 : 1;
}
//Generic sort function
function sortByProp(...props) {
const callback = props.pop();
return function(a, b) {
const v1 = retrieve(props, a);
const v2 = retrieve(props, b);
return callback(v1, v2);
}
}
//Then you just have to call your sort function, and you can chain them
const res = response
.sort(sortByProp('key', 'kind', sortKind))
.sort(sortByProp('key', 'name', sortName));
console.log(res);
You can see here a Working plunker
You missed "return" in this line
else if(a.key.name == username){ a.key.name < b.key.name }
I am trying to write a function to sort my column because it has specific values. I have a lot alarms, and they have 2 states: Active and Not Active. And in case of "Active" I write to column string - "Active" and in case of "Not active" I write to column last seen date like: 24 Jun 2014, 07:36:14.
And the problem that when I use default jqgrid sort function, it sorts unproperly, because I have 2 types data.
And I found that in jqgrid I can write custom sorting functions. So here is my efforts:
var timeRegexPatter = /(\d{2})-(\d{3})-(\d{4}) (\d{2}):(\d{2}):(\d{2})/;
var alarmLastSeenSort = function(a, b, direction) {
if (a === 'Active') { //how to use directions properly?
return 1;
}
else if (b === 'Active') {
return -1;
}
var timeArrA = a.match(timeRegexPatter); //array A
var timeArrB = b.match(timeRegexPatter); //Array B
//here should be probably transform time regex into timestamp and compare it,
//is there any predefined functions to do this
}
Caveat: I don't know the jqgrid library specifically.
In general, though, a sort function should return 1, 0, or -1 based on the comparison of the two incoming arguments. Assuming an ascending sort:
If a < b, return -1
If a == b, return 0
If a > b, return 1
where the <, ==, and > operators refer to your desired collating order of the objects, which may not necessarily be the same as strict mathematical or string comparisons. For example, you may have an object that you want to sort by name, then ID, which would involve comparisons of two different properties with different types.
In your case, you have two axes on which to sort, "activeness" and "timestamp". So your first question is: how should an active compare to an inactive? It doesn't make sense to compare one item at a time, unless it's to forbid sorting of objects that are not the same type and throw an error.
Once you dispose of the "activeness" sort, you can move on to comparing timestamps in the case of an inactive item.
Again, I don't know jqgrid specifically, but I assume direction refers to "ascending" or "descending" order. That will determine whether you return a 1 (ascending) or -1 (descending) for the a > b case.
Demo here.
var i, sortedArray;
var testArray = [
'active',
'2014-06-25 01:23:45',
'active',
'active',
'2013-01-31 12:34:56',
'2014-06-25 02:34:45'];
var comparitor = function(a, b, direction) {
console.log('comparitor(a, b, direction)::', a, b, direction);
// Replace this with whatever test allows you to determine whether you're sorting in ascending or descending order
var after = direction === 'descending' ? -1 : 1;
// If both are active, neither should be reordered with respect to the other
if (a === 'active' && b === 'active') {
console.log('Both a & b are active; returning 0');
return 0;
}
// We know at least one is "inactive". Assume "active" should come before "inactive".
if (a === 'active') {
console.log('a is active; returning -1');
return -1 * after;
} else if (b === 'active') {
console.log('b is active; returning 1');
return after;
}
// We know that neither one is active, and can assume both are date strings. You could convert to dates here, but why, since your dates are already in a format that sorts quite nicely?
if (a === b) {
console.log('a === b; returning 0');
return 0;
}
console.log('a !== b; returning either 1 or -1');
return a > b ? after : -1 * after;
}
sortedArray = testArray.sort(comparitor);
for (i = 0; i < sortedArray.length; i++) {
console.log(i + ' = ' + sortedArray[i]);
}
Background:
As needed in some task, I need a simple sort function. For simplicity, I wrote another function to wrap the built-in sort function as:
function sortBy(obj, extra, func){
if(typeof func == 'function'){
f = func;
} else if(typeof extra != 'function'){
eval('function f(a, b, ai, bi, e){return ' + func + '}');
} else {
var f = extra;
extra = null;
}
var res = [];
for(var i in obj){
if(obj.hasOwnProperty(i)){
obj[i]._k_ = i;
res.push(obj[i]);
}
}
res.sort(function(a, b){
if(f(a, b, a._k_, b._k_, extra)){
return 1;
} else {
return -1;
}
})
return res;
}
My attempts are:
Make it possible to sort a object directly
Keep the original object as the hash table
Allow some simple syntax
For instance,
var data ={
12: {age:27, name:'pop', role: 'Programmer'},
32: {age:25, name:'james', role: 'Accontant'},
123:{age:19, name:'jerry', role:'Sales Representative'},
15:{age:22, name:'jerry', role:'Coder'},
17:{age:19, name:'jerry', role:'Tester'},
43:{age:14, name:'anna', role: 'Manager'},
55: {age:31, name:'luke', role:'Analyst'}
};
There are several usages:
var b = sortBy(data, '', 'a.age < b.age'); // a simple sort, order by age
var b = sortBy(data, 19, 'b.age == e'); // pick up all records of age 19, and put them in the beginning
var b = sortBy(data, function(a, b){return a.name > b.name}); // anonymous sort function is also allowed
QUESTION
Though it works as expected in our code, I would like to raise some question:
Is there any potiential problem about using eval to create sort function from string?
Is there any story about sort function returning -1(nagative), 0 and 1(positive)?
Can we change the code as "return if(f(a, b, a.k, b.k, extra)", instead of returning 1 or -1? We found it works in our firefox and chrome, but not sure whether it is safe to do so.
1. Is there any potiential problem about using eval to create sort function from string?
Not per se, but it does suffer all the same deficiencies as calling other eval-style functions with strings, e.g. setTimeout() or the Function() constructor. As long as you trust the source of the string there's no real problem. However, I would consider using the Function constructor instead:
f = new Function(a, b, ai, bi, e, 'return ' + func);
It's more manageable and it's definitely more appropriate than evaluating a function declaration.
2. Is there any story about sort function returning -1(nagative), 0 and 1(positive)?
Not really understanding this part of your question, but your function doesn't appear to tackle what to do if two items are the same from the comparison. You should be returning less than 0, 0 or more than 0 depending on the result. The best approach for this is to use String.prototype.localeCompare():
return String.prototype.localeCompare.call(a, b);
Try making it so you only do eval on a small part:
(fiddle: http://jsfiddle.net/maniator/SpJbN/)
function sortBy(obj, extra, func){
var f = null;
if(typeof func == 'function'){
f = func;
} else if(typeof extra != 'function'){
f = function(a, b, ai, bi, e){
return eval(func); // <-- smaller part
}
} else {
f = extra;
extra = null;
}
var res = [];
for(var i in obj){
if(obj.hasOwnProperty(i)){
obj[i]._k_ = i;
res.push(obj[i]);
}
}
res.sort(function(a, b){
if(f(a, b, a._k_, b._k_, extra)){
return 1;
} else {
return -1;
}
})
return res;
}
Thanks to Andy, we change the code to
var f = new Function('a, b, ai, bi, e', 'return ' + func);
Note that the arguments should be passed in as string, check out :
https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function
About the second question, I think it is because we were trying to make the sort function more explicitly.
For instance,
sort([1, 2, 3, 5, 1], '', 'a < b') ==> [1, 1, 2, 3, 5]
Literal meaning of 'a < b' to us is "the latter item is greater than the former item", so the array is sorted to [1, 1, 2, 3, 5].
Another example, 'a.age < b.age' will return the records in the order that younger person comes before olders.
That's the reason I am asking can we use true or false instead of -1, 0, 1.
We keep doing some more small tests, and figure out something, would like to share with everyone.
For example:
var b = [
{age:27, name:'pop 2', role: 'Programmer'},
{age:19, name:'james', role: 'Accontant'},
{age:19, name:'jerry', role:'Sales Representative'},
{age:22, name:'jerry', role:'Coder'},
{age:19, name:'jerry', role:'Tester'},
{age:14, name:'anna', role: 'Manager'},
{age:19, name:'luke', role:'Analyst'},
{age:27, name:'pop', role: 'Programmer'},
{age:14, name:'anna 2', role: 'Manager'}
];
b.sort(function(a, b) {return a.age - b.age > 0? 1: -1}); // #1
b.sort(function(a, b) {return a.age > b.age}); // #2
b.sort(function(a, b) {return a.age - b.age}); // #3
Although the above sorts return the same result, try this one :
b.sort(function(a, b) {return a.age - b.age < 0? -1: 1}); // #4
In this statement, the records are still ordered by age, but the order is reversed within a same age group.
#1 and #2 is same as #3 by chance. If the browser use different algorithm to implement the sort function,
it is possible that #1 and #2 will behave like #4. If you are strict with the order of the result, you need
to explicitly return 0, when 'a' equals to item 'b'.
Besides, as Andy pointed out, in certain cases(e.g., #4), it is possible that unnecessary swaps are done if we don't return 0 explicitly, which could affect the performance.
The reason we didn't notice this before is because we didn't care the order within a group, providing the record
is sorted on certain property.
I think it would be appropriate to pass a function that does the comparing. so something like
sort(somedata, function(a, b) {
return a < b;
});