How to sort strings in JavaScript
Asked Answered
A

16

538

I have a list of objects I wish to sort based on a field attr of type string. I tried using -

list.sort(function (a, b) {
    return a.attr - b.attr
})

but found that - doesn't appear to work with strings in JavaScript. How can I sort a list of objects based on an attribute with type string?

Abfarad answered 9/9, 2008 at 3:25 Comment(4)
see JavaScript case insensitive string comparison on #2141127Dative
For a quick "internationalized" solution (only partly I guess as this may not cover all accents in the world), you may want to simply ignore accents, that is, remove them. Then only do your string comparison, see Javascript : remove accents/diacritics in strings on #991404Dative
Funny enough Jeff Atwood himself wrote a blog post about this common issue back in 2007, see blog.codinghorror.com/sorting-for-humans-natural-sort-orderDative
This is a very old question, so if you're coming across this from the future like I did, you should really read this question about performance before implementing any suggestions you find here.Kronstadt
C
924

Use String.prototype.localeCompare as per your example:

list.sort(function (a, b) {
    return ('' + a.attr).localeCompare(b.attr);
})

We force a.attr to be a string to avoid exceptions. localeCompare has been supported since Internet Explorer 6 and Firefox 1. You may also see the following code used that doesn't respect a locale:

if (item1.attr < item2.attr)
  return -1;
if ( item1.attr > item2.attr)
  return 1;
return 0;
Cellobiose answered 9/9, 2008 at 3:29 Comment(11)
Before anyone makes the same hasty mistake as I did, it's localeCompare, not localCompare.Glendoraglendower
The first solution will consider "A" to come after "z" but before "Z" as it's doing a comparison on the character ASCII value. localeCompare() doesn't run into this problem but doesn't understand numerics so you'll get [ "1", "10", "2" ] as with sorting comparisons in most languages. if you want sorting for your UI front end, look into the alphanum/natural sort algorithm #4340727 or #4322329Meaghanmeagher
Note that localeCompare() is only supported in modern browsers: IE11+ at the time of writing, see developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…Dative
One may notice that this question is about Natural Sort Algorithm .... see #2802841Dative
No, I mean the first line of the table, @Adrien - IE supports localeCompare() going back many versions, but does not support specifying the locale until version 11. Note also the questions that Dead.Rabit linked to.Cellobiose
@Cellobiose my bad, it seems like it's supported since IE6! see (scroll-down/search to localeCompare() method) on msdn.microsoft.com/en-us/library/ie/s4esdbwz(v=vs.94).aspx . One thing to note though, In the old implementations where we do not use the locales and options arguments (the one used before IE11) the locale and sort order used are entirely implementation dependent, in other word: Firefox, Safari, Chrome & IE do NOT sort strings in the same order. see code.google.com/p/v8/issues/detail?id=459Dative
@Cellobiose thanks again for your help, after some epic research I added my own answer as I thought this might be enjoyed by the community, see below.Dative
See my related answer on how localeCompare() behaves with different locales spesified. https://mcmap.net/q/36707/-a-stable-neutral-culture-insensitive-locale-in-javascriptGustavogustavus
@Glendoraglendower Copied and pasted. Worked fine.Miscegenation
In this case (or in general), what is the difference between (''+a.attr) and a.attr.toString() ?Corenda
In general: undefined.toString() is a runtime error, while ''+undefined evaluates to 'undefined', @TheCuBeMan. In this specific case, it means if your list contains an item that doesn't have an attr property the sort logic will still function. However, this may mask a more serious error (e.g., you forgot to initialize an object, or mistyped a property name). In general, I prefer to avoid errors in generic bits of functionality like this and leave them to be discovered in places where it's more obvious what went wrong. Credit goes to @robocat who introduced this idiom in their edit!Cellobiose
D
226

An updated answer (October 2014)

I was really annoyed about this string natural sorting order so I took quite some time to investigate this issue.

Long story short

localeCompare() character support is badass; just use it. As pointed out by Shog9, the answer to your question is:

return item1.attr.localeCompare(item2.attr);

Bugs found in all the custom JavaScript "natural string sort order" implementations

There are quite a bunch of custom implementations out there, trying to do string comparison more precisely called "natural string sort order"

When "playing" with these implementations, I always noticed some strange "natural sorting order" choice, or rather mistakes (or omissions in the best cases).

Typically, special characters (space, dash, ampersand, brackets, and so on) are not processed correctly.

You will then find them appearing mixed up in different places, typically that could be:

  • some will be between the uppercase 'Z' and the lowercase 'a'
  • some will be between the '9' and the uppercase 'A'
  • some will be after lowercase 'z'

When one would have expected special characters to all be "grouped" together in one place, except for the space special character maybe (which would always be the first character). That is, either all before numbers, or all between numbers and letters (lowercase & uppercase being "together" one after another), or all after letters.

My conclusion is that they all fail to provide a consistent order when I start adding barely unusual characters (i.e., characters with diacritics or characters such as dash, exclamation mark and so on).

Research on the custom implementations:

Browsers' native "natural string sort order" implementations via localeCompare()

localeCompare() oldest implementation (without the locales and options arguments) is supported by Internet Explorer 6 and later, see Legacy Microsoft Edge developer documentation (scroll down to the localeCompare() method).

The built-in localeCompare() method does a much better job at sorting, even international and special characters.

The only problem using the localeCompare() method is that "the locale and sort order used are entirely implementation dependent". In other words, when using localeCompare such as stringOne.localeCompare(stringTwo): Firefox, Safari, Chrome, and Internet Explorer have a different sort order for Strings.

Research on the browser-native implementations:

Difficulty of "string natural sorting order"

Implementing a solid algorithm (meaning: consistent but also covering a wide range of characters) is a very tough task. UTF-8 contains more than 2000 characters and covers more than 120 scripts (languages).

Finally, there are some specification for this tasks, it is called the "Unicode Collation Algorithm". You can find more information about this on this question I posted, Is there a language-agnostic specification for "string natural sorting order"?

Final conclusion

So considering the current level of support provided by the JavaScript custom implementations I came across, we will probably never see anything getting any close to supporting all these characters and scripts (languages). Hence I would rather use the browsers' native localeCompare() method. Yes, it does have the downside of being non-consistent across browsers, but basic testing shows it covers a much wider range of characters, allowing solid and meaningful sort orders.

So as pointed out by Shog9, the answer to your question is:

return item1.attr.localeCompare(item2.attr);

Further reading:

Thanks to Shog9's nice answer, which put me in the "right" direction I believe.

Dative answered 10/10, 2014 at 8:39 Comment(1)
The link near "scroll down to" is (effectively) broken. It redirects to an unspecific page (localeCompare() is not on it).Teena
C
75

Answer (in Modern ECMAScript)

list.sort((a, b) => (a.attr > b.attr) - (a.attr < b.attr))

Or

list.sort((a, b) => +(a.attr > b.attr) || -(a.attr < b.attr))

Description

Casting a boolean value to a number yields the following:

  • true -> 1
  • false -> 0

Consider three possible patterns:

  • x is larger than y: (x > y) - (y < x) -> 1 - 0 -> 1
  • x is equal to y: (x > y) - (y < x) -> 0 - 0 -> 0
  • x is smaller than y: (x > y) - (y < x) -> 0 - 1 -> -1

(Alternative)

  • x is larger than y: +(x > y) || -(x < y) -> 1 || 0 -> 1
  • x is equal to y: +(x > y) || -(x < y) -> 0 || 0 -> 0
  • x is smaller than y: +(x > y) || -(x < y) -> 0 || -1 -> -1

So these logics are equivalent to typical sort comparator functions.

if (x == y) {
    return 0;
}
return x > y ? 1 : -1;
Countermove answered 1/11, 2016 at 6:28 Comment(7)
As I commented on the earlier answer that used this trick, code-only answers can be made more useful by explaining how they work.Mixie
Added descriptionCountermove
Can you comment on whether this is better or worse than localeCompare?Dappled
@RanLottem localeCompare and standard comparison yield different results. Which do you expect? ["A", "b", "C", "d"].sort((a, b) => a.localeCompare(b)) sorts in case-insensitive alphabetic order while ["A", "b", "C", "d"].sort((a, b) => (a > b) - (a < b)) does in codepoint orderCountermove
I see, that seems to be a main sticking point. Any idea about performance differences?Dappled
I guess that codepoint order perform faster, however, I recommend you to confirm it by your benchmarking.Countermove
This is better than localeCompare for the reason that localeCompare will return 0 for strings that are not equal. Concrete example: There are (at least) two different "Ö" symbols that look the same, and localeCompare says they're the same, but they fail === (even when uppercased). So your pretty UI code may do something involving sorting and grouping, and thing that "Ö" is the same as "Ö" but your back end logic that uses a Map will decide that those two Ö's are different, so things turn out badly.Kelci
T
29

Since strings can be compared directly in JavaScript, this will do the job:

list.sort(function (a, b) {
    return a.attr < b.attr ? -1: 1;
})

This is a little bit more efficient than using

  return a.attr > b.attr ? 1: -1;

because in case of elements with same attr (a.attr == b.attr), the sort function will swap the two for no reason.

For example

  var so1 = function (a, b) { return a.atr > b.atr ? 1: -1; };
  var so2 = function (a, b) { return a.atr < b.atr ? -1: 1; }; // Better

  var m1 = [ { atr: 40, s: "FIRST" }, { atr: 100, s: "LAST" }, { atr: 40, s: "SECOND" } ].sort (so1);
  var m2 = [ { atr: 40, s: "FIRST" }, { atr: 100, s: "LAST" }, { atr: 40, s: "SECOND" } ].sort (so2);

  // m1 sorted but ...:  40 SECOND  40 FIRST   100 LAST
  // m2 more efficient:  40 FIRST   40 SECOND  100 LAST
Taut answered 22/9, 2019 at 13:34 Comment(3)
This is not exactly correct, the comparison function must return 0 when the two strings are equalIngredient
the list will be sorted correctly anyway using this comparison function but if not moving elements with same value matters, yes it can be changed by "return a.attr == b.attr ? 0: a.attr > b.attr ? 1: -1;"Taut
actually comparation a.attr == b.attr is not needed because the result 0 from the function would tell the sorting algorithm to do nothing , the same as the result -1.Taut
A
16

You should use > or < and == here. So the solution would be:

list.sort(function(item1, item2) {
    var val1 = item1.attr,
        val2 = item2.attr;
    if (val1 == val2) return 0;
    if (val1 > val2) return 1;
    if (val1 < val2) return -1;
});
Abfarad answered 9/9, 2008 at 3:29 Comment(1)
On a side note, this won't handle string vs number comparisons. For example: 'Z' < 9 (false), 'Z' > 9 (also false??), 'Z' == 9 (also false!!). Silly NaN in JavaScript...Perdure
C
14

Nested ternary arrow function

(a,b) => (a < b ? -1 : a > b ? 1 : 0)
Connate answered 14/3, 2019 at 1:55 Comment(0)
K
10

A TypeScript sorting method modifier using a custom function to return a sorted string in either ascending or descending order:

const data = ["jane", "mike", "salome", "ababus", "buisa", "dennis"];

const sortStringArray = (stringArray: string[], mode?: 'desc' | 'asc') => {
  if (!mode || mode === 'asc') {
    return stringArray.sort((a, b) => a.localeCompare(b))
  }
  return stringArray.sort((a, b) => b.localeCompare(a))
}

console.log(sortStringArray(data, 'desc'));// [ 'salome', 'mike', 'jane', 'dennis', 'buisa', 'ababus' ]
console.log(sortStringArray(data, 'asc')); // [ 'ababus', 'buisa', 'dennis', 'jane', 'mike', 'salome' ]
Kiley answered 21/12, 2021 at 2:39 Comment(0)
M
9

An explanation of why the approach in the question doesn't work:

let products = [
    { name: "laptop", price: 800 },
    { name: "phone", price:200},
    { name: "tv", price: 1200}
];
products.sort( (a, b) => {
    {let value= a.name - b.name; console.log(value); return value}
});

> 2 NaN

Subtraction between strings returns NaN.

Echoing Alejadro's answer, the right approach is:

products.sort( (a,b) => a.name > b.name ? 1 : -1 )

Motorcar answered 30/10, 2020 at 16:5 Comment(0)
G
8

I had been bothered about this for long, so I finally researched this and give you this long winded reason for why things are the way they are.

From the spec:

Section 11.9.4   The Strict Equals Operator ( === )

The production EqualityExpression : EqualityExpression === RelationalExpression
is evaluated as follows: 
- Let lref be the result of evaluating EqualityExpression.
- Let lval be GetValue(lref).
- Let rref be the result of evaluating RelationalExpression.
- Let rval be GetValue(rref).
- Return the result of performing the strict equality comparison 
  rval === lval. (See 11.9.6)

So now we go to 11.9.6

11.9.6   The Strict Equality Comparison Algorithm

The comparison x === y, where x and y are values, produces true or false. 
Such a comparison is performed as follows: 
- If Type(x) is different from Type(y), return false.
- If Type(x) is Undefined, return true.
- If Type(x) is Null, return true.
- If Type(x) is Number, then
...
- If Type(x) is String, then return true if x and y are exactly the 
  same sequence of characters (same length and same characters in 
  corresponding positions); otherwise, return false.

That's it. The triple equals operator applied to strings returns true iff the arguments are exactly the same strings (same length and same characters in corresponding positions).

So === will work in the cases when we're trying to compare strings which might have arrived from different sources, but which we know will eventually have the same values - a common enough scenario for inline strings in our code. For example, if we have a variable named connection_state, and we wish to know which one of the following states ['connecting', 'connected', 'disconnecting', 'disconnected'] is it in right now, we can directly use the ===.

But there's more. Just above 11.9.4, there is a short note:

NOTE 4     
  Comparison of Strings uses a simple equality test on sequences of code 
  unit values. There is no attempt to use the more complex, semantically oriented
  definitions of character or string equality and collating order defined in the 
  Unicode specification. Therefore Strings values that are canonically equal
  according to the Unicode standard could test as unequal. In effect this 
  algorithm assumes that both Strings are already in normalized form.

Hmm. What now? Externally obtained strings can, and most likely will, be weird unicodey, and our gentle === won't do them justice. In comes localeCompare to the rescue:

15.5.4.9   String.prototype.localeCompare (that)
    ...
    The actual return values are implementation-defined to permit implementers 
    to encode additional information in the value, but the function is required 
    to define a total ordering on all Strings and to return 0 when comparing
    Strings that are considered canonically equivalent by the Unicode standard. 

We can go home now.

tl;dr;

To compare strings in javascript, use localeCompare; if you know that the strings have no non-ASCII components because they are, for example, internal program constants, then === also works.

Gerbil answered 7/2, 2013 at 18:6 Comment(0)
J
6

If you want to control locales (or case or accent), then use Intl.collator:

const collator = new Intl.Collator();
list.sort((a, b) => collator.compare(a.attr, b.attr));

You can construct a collator like:

new Intl.Collator("en");
new Intl.Collator("en", {sensitivity: "case"});
...

See the above link for documentation.

Note: unlike some other solutions, it handles null, undefined the JavaScript way, i.e., moves them to the end.

Jasminejason answered 28/1, 2022 at 8:36 Comment(0)
C
5

There should be ascending and descending orders functions

if (order === 'asc') {
  return a.localeCompare(b);
}
return b.localeCompare(a);
Chatter answered 24/6, 2021 at 21:45 Comment(0)
I
4

Use sort() straightforward without any - or <

const areas = ['hill', 'beach', 'desert', 'mountain']
console.log(areas.sort())

// To print in descending way
console.log(areas.sort().reverse())
Ideal answered 31/1, 2021 at 12:32 Comment(2)
this doesn't work for example ['42', '2', '40', '44', '47', '12', '41', '13', '5', '9', '14', '6', '20', '25', '59', '46', '3', '4', '72', '52', '53', '21', '26', '54', '75', '57', '19', '22', '56', '17', '16', '60', '61', '64'].sort()Thunderstone
@VirajSingh it works. String comparison is not number comparison. It doesn't works for complex data (i.e.: array of objects)Perpend
R
0

In your operation in your initial question, you are performing the following operation:

item1.attr - item2.attr

So, assuming those are numbers (i.e. item1.attr = "1", item2.attr = "2") You still may use the "===" operator (or other strict evaluators) provided that you ensure type. The following should work:

return parseInt(item1.attr) - parseInt(item2.attr);

If they are alphaNumeric, then do use localCompare().

Raycher answered 24/10, 2013 at 18:15 Comment(0)
K
0
list.sort(function(item1, item2){
    return +(item1.attr > item2.attr) || +(item1.attr === item2.attr) - 1;
}) 

How they work samples:

+('aaa'>'bbb')||+('aaa'==='bbb')-1
+(false)||+(false)-1
0||0-1
-1

+('bbb'>'aaa')||+('bbb'==='aaa')-1
+(true)||+(false)-1
1||0-1
1

+('aaa'>'aaa')||+('aaa'==='aaa')-1
+(false)||+(true)-1
0||1-1
0
Koran answered 1/9, 2016 at 21:41 Comment(1)
Code-only answers can be made more useful by explaining how they work.Mixie
F
-4
<!doctype html>
<html>
<body>
<p id = "myString">zyxtspqnmdba</p>
<p id = "orderedString"></p>
<script>
var myString = document.getElementById("myString").innerHTML;
orderString(myString);
function orderString(str) {
    var i = 0;
    var myArray = str.split("");
    while (i < str.length){
        var j = i + 1;
        while (j < str.length) {
            if (myArray[j] < myArray[i]){
                var temp = myArray[i];
                myArray[i] = myArray[j];
                myArray[j] = temp;
            }
            j++;
        }
        i++;
    }
    var newString = myArray.join("");
    document.getElementById("orderedString").innerHTML = newString;
}
</script>
</body>
</html>
Fete answered 23/4, 2018 at 20:45 Comment(2)
Please add some infos on how this is going to solve the question to your answer. Code-only answers are not welcomed. Thank you.Ymir
here you want to order the characters within a string, which is not what is asked. You can achieve this sorting simply using "Array.sort" e.g. str.split("").sort ().join("")Taut
A
-4
var str = ['v','a','da','c','k','l']
var b = str.join('').split('').sort().reverse().join('')
console.log(b)
Ambiversion answered 9/4, 2019 at 10:29 Comment(2)
While this code may solve the question, including an explanation of how and why this solves the problem would really help to improve the quality of your post, and probably result in more up-votes. Remember that you are answering the question for readers in the future, not just the person asking now. Please edit your answer to add explanation, and give an indication of what limitations and assumptions apply.Moidore
This example doesn't works. You end up with da separated if you try splitting the resultPerpend

© 2022 - 2024 — McMap. All rights reserved.