New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Array#sort() inconsistent implementations (support for booleans) #902

Open
Reinmar opened this Issue Apr 20, 2017 · 17 comments

Comments

Projects
None yet
7 participants
@Reinmar

Reinmar commented Apr 20, 2017

Hi,

I'm not sure that this is the right place to report this and I'm quite sure it must've been discussed in the past, but let's see.

I found out today that:

[ 1, 4, 2, 6, 0, 6, 2, 6 ].sort( ( a, b ) => a > b )

gives a sorted array in Chrome and Firefox and does nothing in Edge and Safari.

Obviously, sort()'s callback is meant to return a number, but, apparently, two engines also support booleans.

Does it make Chrome's and Firefox's behaviour a bug? Would it be worth (and, first of all, possible) to align Chrome and Firefox to the spec?

Or, since that would be a backwards incompatible change and taken that sort() doesn't need to implement a stable sorting algorithm anyway, a support for booleans could be standardised?

@Reinmar Reinmar referenced this issue Apr 20, 2017

Closed

MS Edge support #423

@mathiasbynens

This comment has been minimized.

Show comment
Hide comment
@mathiasbynens

mathiasbynens Apr 20, 2017

Member

A comparefn that does not return numeric values is not a consistent comparison function. In that case, the sort order is implementation-defined.

From https://tc39.github.io/ecma262/#sec-array.prototype.sort:

If comparefn is not undefined and is not a consistent comparison function for the elements of this array (see below), the sort order is implementation-defined.

And below:

A function comparefn is a consistent comparison function for a set of values S if all of the requirements below are met […]
[…]
Furthermore, Type(v) is Number

You could file bugs trying to get the engines to agree on their behavior, after which the behavior could potentially be specified.

Member

mathiasbynens commented Apr 20, 2017

A comparefn that does not return numeric values is not a consistent comparison function. In that case, the sort order is implementation-defined.

From https://tc39.github.io/ecma262/#sec-array.prototype.sort:

If comparefn is not undefined and is not a consistent comparison function for the elements of this array (see below), the sort order is implementation-defined.

And below:

A function comparefn is a consistent comparison function for a set of values S if all of the requirements below are met […]
[…]
Furthermore, Type(v) is Number

You could file bugs trying to get the engines to agree on their behavior, after which the behavior could potentially be specified.

@Reinmar

This comment has been minimized.

Show comment
Hide comment
@Reinmar

Reinmar Apr 20, 2017

Thanks. I haven't even thought that Chrome and Firefox work within the spec's limit. I guess I could open tickets for Edge and Safari so they add support for booleans but I don't think that this has a big chances of passing.

Reinmar commented Apr 20, 2017

Thanks. I haven't even thought that Chrome and Firefox work within the spec's limit. I guess I could open tickets for Edge and Safari so they add support for booleans but I don't think that this has a big chances of passing.

@claudepache

This comment has been minimized.

Show comment
Hide comment
@claudepache

claudepache Apr 20, 2017

Contributor

The comparison function is supposed to return a number, and if it doesn’t, the result is coerced to a number: see step 4.a of SortCompare abstract operation. In particular, Boolean values will be coerced to 0 or 1. (Historical note: that coercion behaviour was added in ES6, because it was observed that it is what the implementations did.)

In the context of Array#sort, a consistent comparison function is a comparison function that models correctly an order relation according to which the set of values will be sorted, as explained in more details at the end of Section Array.prototype.sort. A comparison function that produces 0 or 1 but never -1 is certainly not consistent.

Contributor

claudepache commented Apr 20, 2017

The comparison function is supposed to return a number, and if it doesn’t, the result is coerced to a number: see step 4.a of SortCompare abstract operation. In particular, Boolean values will be coerced to 0 or 1. (Historical note: that coercion behaviour was added in ES6, because it was observed that it is what the implementations did.)

In the context of Array#sort, a consistent comparison function is a comparison function that models correctly an order relation according to which the set of values will be sorted, as explained in more details at the end of Section Array.prototype.sort. A comparison function that produces 0 or 1 but never -1 is certainly not consistent.

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Apr 20, 2017

Member

@Reinmar what you probably want is [1, 4, 2, 6, 0, 6, 2, 6].sort((a, b) => a - b) (or b - a). For strings, a comparator can instead return a.localeCompare(b).

Member

ljharb commented Apr 20, 2017

@Reinmar what you probably want is [1, 4, 2, 6, 0, 6, 2, 6].sort((a, b) => a - b) (or b - a). For strings, a comparator can instead return a.localeCompare(b).

@erights

This comment has been minimized.

Show comment
Hide comment
@erights

erights Apr 20, 2017

erights commented Apr 20, 2017

@allenwb

This comment has been minimized.

Show comment
Hide comment
@allenwb

allenwb Apr 20, 2017

Member
Member

allenwb commented Apr 20, 2017

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Apr 20, 2017

Member

Despite that, the steps for comparison functions (see https://tc39.github.io/ecma262/#sec-sortcompare step 4.b) treats it as +0, even though the consistency requirements require that it not return NaN.

Member

ljharb commented Apr 20, 2017

Despite that, the steps for comparison functions (see https://tc39.github.io/ecma262/#sec-sortcompare step 4.b) treats it as +0, even though the consistency requirements require that it not return NaN.

@erights

This comment has been minimized.

Show comment
Hide comment
@erights

erights Apr 21, 2017

Interesting. I didn't know about this conversion to +0. But even if we allowed comparefn to return NaN and treated it as if it had returned +0, (a, b) => a - b would still not be transitive, and so still not a consistent comparison function.

No action item here re NaN. I can't think of any realistic way to further improve the situation.

erights commented Apr 21, 2017

Interesting. I didn't know about this conversion to +0. But even if we allowed comparefn to return NaN and treated it as if it had returned +0, (a, b) => a - b would still not be transitive, and so still not a consistent comparison function.

No action item here re NaN. I can't think of any realistic way to further improve the situation.

@claudepache

This comment has been minimized.

Show comment
Hide comment
@claudepache

claudepache Apr 21, 2017

Contributor

Despite that, the steps for comparison functions (see https://tc39.github.io/ecma262/#sec-sortcompare step 4.b) treats it as +0, even though the consistency requirements require that it not return NaN.

It is probably a bug that the consistency requirements still require a non-NaN Number when the value is coerced to a non-NaN number in SortCompare anyway.

Or, in other words, the consistency conditions should apply to SortCompare(a, b) rather than comparefn(a, b).

Contributor

claudepache commented Apr 21, 2017

Despite that, the steps for comparison functions (see https://tc39.github.io/ecma262/#sec-sortcompare step 4.b) treats it as +0, even though the consistency requirements require that it not return NaN.

It is probably a bug that the consistency requirements still require a non-NaN Number when the value is coerced to a non-NaN number in SortCompare anyway.

Or, in other words, the consistency conditions should apply to SortCompare(a, b) rather than comparefn(a, b).

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Apr 21, 2017

Member

I agree; it seems that either NaN should be allowed in consistent comparison functions, or SortCompare shouldn't need to mention it.

Member

ljharb commented Apr 21, 2017

I agree; it seems that either NaN should be allowed in consistent comparison functions, or SortCompare shouldn't need to mention it.

@Reinmar

This comment has been minimized.

Show comment
Hide comment
@Reinmar

Reinmar May 17, 2017

Webkit aligned its implementation to that of Firefox and Chrome :) https://bugs.webkit.org/show_bug.cgi?id=47825

Reinmar commented May 17, 2017

Webkit aligned its implementation to that of Firefox and Chrome :) https://bugs.webkit.org/show_bug.cgi?id=47825

@claudepache

This comment has been minimized.

Show comment
Hide comment
@claudepache

claudepache May 18, 2017

Contributor

Webkit aligned its implementation to that of Firefox and Chrome :) https://bugs.webkit.org/show_bug.cgi?id=47825

And what is the semantics of Firefox and Chrome?

Their patch seems to special-case boolean results of the comparison function, but I’m not sure it is what Firefox or Chrome do. Indeed, consider the following testcases, where the comparison function does not return a Boolean:

[1,3,2,5,4].sort((a,b) => Number(b<a)) // [ 1, 2, 3, 4, 5 ] in both FF in Chrome
[1,3,2,5,4].sort((a,b) => Number(a<b))  // [ 5, 4, 3, 2, 1 ] in both FF in Chrome

A reasonable semantics to me would be the following: If compareFn is the comparison function and if ToNumber(compareFn(a, b)) evaluates to 0 (or NaN), evaluates ToNumber(compareFn(b, a)) in order to decide how a and b should be ordered. But what they really do, I don’t know: I’ve not read the source code of FF or Chrome.

Contributor

claudepache commented May 18, 2017

Webkit aligned its implementation to that of Firefox and Chrome :) https://bugs.webkit.org/show_bug.cgi?id=47825

And what is the semantics of Firefox and Chrome?

Their patch seems to special-case boolean results of the comparison function, but I’m not sure it is what Firefox or Chrome do. Indeed, consider the following testcases, where the comparison function does not return a Boolean:

[1,3,2,5,4].sort((a,b) => Number(b<a)) // [ 1, 2, 3, 4, 5 ] in both FF in Chrome
[1,3,2,5,4].sort((a,b) => Number(a<b))  // [ 5, 4, 3, 2, 1 ] in both FF in Chrome

A reasonable semantics to me would be the following: If compareFn is the comparison function and if ToNumber(compareFn(a, b)) evaluates to 0 (or NaN), evaluates ToNumber(compareFn(b, a)) in order to decide how a and b should be ordered. But what they really do, I don’t know: I’ve not read the source code of FF or Chrome.

@claudepache

This comment has been minimized.

Show comment
Hide comment
@claudepache

claudepache May 18, 2017

Contributor

Other testcases (same results in both FF and Chrome):

[1,3,2,5,4].sort((a,b) => 1.5*Number(b<a)) // [ 1, 2, 3, 4, 5 ]
[1,3,2,5,4].sort((a,b) => 1.5*Number(a<b))  // [ 5, 4, 3, 2, 1 ]

but (showing that they do not follow the “reasonable” semantics of my previous comment):

[1,3,2,5,4].sort((a,b) => -Number(b<a)) // [ 1, 3, 2, 5, 4 ] (not sorted)
[1,3,2,5,4].sort((a,b) => -Number(a<b)) // [ 1, 3, 2, 5, 4 ] (not sorted)
Contributor

claudepache commented May 18, 2017

Other testcases (same results in both FF and Chrome):

[1,3,2,5,4].sort((a,b) => 1.5*Number(b<a)) // [ 1, 2, 3, 4, 5 ]
[1,3,2,5,4].sort((a,b) => 1.5*Number(a<b))  // [ 5, 4, 3, 2, 1 ]

but (showing that they do not follow the “reasonable” semantics of my previous comment):

[1,3,2,5,4].sort((a,b) => -Number(b<a)) // [ 1, 3, 2, 5, 4 ] (not sorted)
[1,3,2,5,4].sort((a,b) => -Number(a<b)) // [ 1, 3, 2, 5, 4 ] (not sorted)
@Yaffle

This comment has been minimized.

Show comment
Hide comment
@Yaffle

Yaffle May 18, 2017

Contributor

if some implementation uses
comparefn(a, b) < 0 internally, (a > b ? 1 : 0) < 0 gives false and probably the result will not be sorted.

Will it make implementations consistent if spec, that implementation should use > 0 for the result of comparefn(a, b) - comparefn(a, b) > 0 ?

Contributor

Yaffle commented May 18, 2017

if some implementation uses
comparefn(a, b) < 0 internally, (a > b ? 1 : 0) < 0 gives false and probably the result will not be sorted.

Will it make implementations consistent if spec, that implementation should use > 0 for the result of comparefn(a, b) - comparefn(a, b) > 0 ?

@claudepache

This comment has been minimized.

Show comment
Hide comment
@claudepache

claudepache May 18, 2017

Contributor

Testcase:

function test(numValues, numDistinctValues, compareFn) {
    var arr = Array(numValues)
    for (let i = 0; i < numValues; i++) {
        arr[i] = Math.floor(Math.random()*numDistinctValues)
    }
    arr.sort(compareFn)
    for (let i = 1; i < numValues; i++) {
        if (arr[i] < arr[i - 1])
            return false
    }
    return true
}

Naturally, test(n, m, (a,b) => a - b) must return true for every n and m.

For test(n, m, (a,b) => a - b > 0), I get the following results:

  • Firefox: always true;
  • Chrome: true for n ≤ 10, usually false otherwise;
  • Edge and Safari: usually false.
Contributor

claudepache commented May 18, 2017

Testcase:

function test(numValues, numDistinctValues, compareFn) {
    var arr = Array(numValues)
    for (let i = 0; i < numValues; i++) {
        arr[i] = Math.floor(Math.random()*numDistinctValues)
    }
    arr.sort(compareFn)
    for (let i = 1; i < numValues; i++) {
        if (arr[i] < arr[i - 1])
            return false
    }
    return true
}

Naturally, test(n, m, (a,b) => a - b) must return true for every n and m.

For test(n, m, (a,b) => a - b > 0), I get the following results:

  • Firefox: always true;
  • Chrome: true for n ≤ 10, usually false otherwise;
  • Edge and Safari: usually false.
@claudepache

This comment has been minimized.

Show comment
Hide comment
@claudepache

claudepache May 18, 2017

Contributor

While we’re at it, let’s test the stability of the sort algorithm (https://esdiscuss.org/topic/stable-sort-proposal):

function test(numValues, numDistinctValues, compareFn) {
    "use strict"
    
    let arr = Array(numValues)
    
    for (let i = 0; i < numValues; i++) {
        arr[i] = [ i, Math.floor(Math.random()*numDistinctValues) ]
    }
    
    arr.sort((a, b) => compareFn(a[1], b[1]))
    
    let correct = true
    let stable = true
    for (let i = 1; i < numValues; i++) {
        if (arr[i][1] < arr[i-1][1])
            correct = false
        if (arr[i][1] === arr[i-1][1] && arr[i][0] < arr[i-1][0])
            stable = false
    }
    return [ correct, stable ]
}

(To be tested with numDistinctValues small relatively to numValues for meaningful result about stability.)

Conclusion:

  • Firefox and Safari always use a stable sort;
  • Edge uses a stable sort for n ≤ 512 and an unstable sort for larger arrays;
  • Chrome uses a stable sort for n ≤ 10 and an unstable sort for larger arrays.
Contributor

claudepache commented May 18, 2017

While we’re at it, let’s test the stability of the sort algorithm (https://esdiscuss.org/topic/stable-sort-proposal):

function test(numValues, numDistinctValues, compareFn) {
    "use strict"
    
    let arr = Array(numValues)
    
    for (let i = 0; i < numValues; i++) {
        arr[i] = [ i, Math.floor(Math.random()*numDistinctValues) ]
    }
    
    arr.sort((a, b) => compareFn(a[1], b[1]))
    
    let correct = true
    let stable = true
    for (let i = 1; i < numValues; i++) {
        if (arr[i][1] < arr[i-1][1])
            correct = false
        if (arr[i][1] === arr[i-1][1] && arr[i][0] < arr[i-1][0])
            stable = false
    }
    return [ correct, stable ]
}

(To be tested with numDistinctValues small relatively to numValues for meaningful result about stability.)

Conclusion:

  • Firefox and Safari always use a stable sort;
  • Edge uses a stable sort for n ≤ 512 and an unstable sort for larger arrays;
  • Chrome uses a stable sort for n ≤ 10 and an unstable sort for larger arrays.
@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Mar 21, 2018

Member

Is the next step here removing step 4.b of SortCompare, since a consistent comparison function can't return NaN?

Member

ljharb commented Mar 21, 2018

Is the next step here removing step 4.b of SortCompare, since a consistent comparison function can't return NaN?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment