1

There have been other questions about How to compare arrays in JavaScript?. What I want to know is the most straightforward way to write/use a three-way comparison function like the one required by Array.sort(). Here's an example using the default one which doesn't work well:

> [ [4,5,10], [4,5,6], [4,1,2] ].sort() // no compare function, uses the default one
[ [ 4, 1, 2 ],
  [ 4, 5, 10 ], // oops, string sorting makes 10 < 6
  [ 4, 5, 6 ] ]

This is what I came up with:

// return -1 if lhs is "less" than rhs, +1 if "greater", and 0 if equal
// if lhs and rhs have different lengths, only the shorter part will be considered
function compareArrays(lhs, rhs) {
  for (var ii = 0; ii < lhs.length; ii++) {
    if (lhs[ii] < rhs[ii]) {
      return -1;
    } else if (lhs[ii] > rhs[ii]) {
      return 1;
    }
  }
  return 0;
}

Which gives us what we want:

> [ [4,5,10], [4,5,6], [4,1,2] ].sort(compareArrays)
[ [ 4, 1, 2 ],
  [ 4, 5, 6 ],
  [ 4, 5, 10 ] ]

Is there something more like a one-liner, or must I define my own function whenever I want to do this?

Supporting old browsers is not a requirement. Using libraries like jQuery or Underscore is OK.

One way to look at this is "The first nonzero value from a standard three-way compare applied to each pair of elements." But even then I haven't found a good fit in existing libraries.

Community
  • 1
  • 1
John Zwinck
  • 239,568
  • 38
  • 324
  • 436
  • Isn't your current solution a "one liner"? You could reduce the sort function to an almost one liner using [*forEach*](http://ecma-international.org/ecma-262/5.1/#sec-15.4.4.18) and the conditional operator [`?:`](http://ecma-international.org/ecma-262/5.1/#sec-11.12), but that's just going to reduce the amount of code by a few characters in one place, not make it faster or more reliable. – RobG May 27 '14 at 05:59
  • 1
    It's not going to get much smaller than your `compareArrays()` function. – jfriend00 May 27 '14 at 06:00
  • "Using libraries like jQuery or Underscore is OK." - Well then, turn your function into a library. There is your one-liner. – user123444555621 May 27 '14 at 06:52
  • Btw, your function does not work for arrays of different length. – Bergi May 27 '14 at 09:35

3 Answers3

1

Probably not the shortest possible, but the fewest lines I can get to sensibly is:

function compareArrays(lhs, rhs) {
  var result;
  lhs.some(function(v, i) {
    return (result = v - rhs[i]);
  });
  return result;
}

or less sensibly:

function compareArrays(lhs, rhs, r) {
  lhs.some(function(v, i) {return (r = v - rhs[i])});
  return r;
}

Edit

Seems you don't want numbers. The compare part can be any relationship you want, e.g. for strings:

function compareArrays(lhs, rhs, r) {
  lhs.some(function(v, i) {return (r = v < rhs[i]? -1 : v > rhs[i]? 1 : 0)});
  return r;
}

[['a','b','c'],['a','c','d'],['a','b','d']].sort(compareArrays) // a,b,c,a,b,d,a,c,d 

But the compare function needs to know what it's getting so it doesn't sort numbers as strings or strings as numbers (and so on…).

RobG
  • 142,382
  • 31
  • 172
  • 209
  • Thanks--this use of `some()` is interesting and fits my conceptual formulation of the problem. Now if only there were a standard "three-way compare" function matching the semantics of the one applied by after to-stringing in `sort()`.... – John Zwinck May 27 '14 at 06:50
  • On further consideration, I realize that what I'd really want from `some()` is more like "Return the first truthy value produced by the callback, or a specified default value if none found." I looked for something like this in Underscore and Lo-dash and came up empty-handed. – John Zwinck May 27 '14 at 07:12
  • This function does not work for arrays of different length. Do you think you can adapt it easily? – Bergi May 27 '14 at 09:36
  • @JohnZwinck—isn't that what my answer does? [*some*](http://ecma-international.org/ecma-262/5.1/#sec-15.4.4.17) returns the first truthy result or defaults to 0 if none found. – RobG May 27 '14 at 09:36
  • @Bergi—yes, provided rules are provided on how to handle them, e.g. include an `i in rhs` test or similar and a value to return if it returns false. – RobG May 27 '14 at 09:38
  • @RobG: that's not what the documentation says nor what my implementation does: some() returns true when the first truthy result is produced by the callback. It does not return the result produced by the callback. – John Zwinck May 27 '14 at 10:05
  • @JohnZwinck—ah yes, I should have said `compareArrays` returns…. Are you trying to replace the compare function with a single expression using only built–in Array methods? *some*, *reduce* and *filter* all have aspects that are close, but none actually does it. – RobG May 27 '14 at 11:43
1

I'd go with a generic comparison-maker function to be used in a functional way:

function compareArrays(compareItems) {
    return function(a, b) {
        for (var r, i=0, l=Math.min(a.length, b.length); i<l; i++)
            if (0 != (r = compareItems(a[i], b[i])))
                return r;
        return a.length - b.length;
     };
}
// Examples:
var compareNumberArrays = compareArray(function(a,b){ return a-b; }),
    compareGenericArrays = compareArray(function(a,b){ return +(a>b)||-(b>a); });

Now you can use

[ [4,5,10], [4,5,6], [4,1,2], [4,5] ].sort(compareNumberArrays)

Is there something more like a one-liner, or must I define my own function whenever I want to do this?

Comparing arrays is too complex for a one-liner, you should use a helper function. There's no built-in one that you can use everywhere.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
0

If you know that they are always triplets of numbers (arrays of length 3), you could do this:

function combineNum(array) {
    return (array[0] * 100) + (array[1] * 10) + array[2];
}

function compareArrays(lhs, rhs) {
    return combineNum(rhs) - combineNum(lhs);
}
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • That also assumes the values in the arrays are always numeric, which is not always the case for me. – John Zwinck May 27 '14 at 06:10
  • @JohnZwinck - yes it does assume that. All your examples were numeric so I thought that was the problem you were trying to solve. Oh well, that was my shot at shortening it. So, what is your real objective here? You have a working example of code. There only needs to be one copy of it in your project and you can then just refer to it. Why are you trying to shave a line or two off it? – jfriend00 May 27 '14 at 06:12
  • It seemed like something that should be possible without hauling around such a trivial function. I guess I'm accustomed to languages where things like comparing arrays is more built-in...but even then, this felt like something that jQuery or Underscore should be able to do without such tedium. Also, even my working function is not optimal: it wastes time if lhs.length is very different from rhs.length...I could add more code to fix that, but this is basically a waste of time if there's an existing function that could do it. – John Zwinck May 27 '14 at 06:55
  • That's also assuming that they are single digits (or at least of equal magnitude), not any numbers. – Bergi May 27 '14 at 06:56
  • @Bergi - Yes, if you want to allow much larger numbers, you can spread out the weighting factors as much as you want. Just trying to offer another conceptual idea here that doesn't have to do so many cell by cell comparisons. – jfriend00 May 27 '14 at 06:57
  • @JohnZwinck - there's nothing built into JS to do this. Heck JS can't even sort a regular single numeric array properly without a custom sort function. So, you build your own library function to do it, include it in your project and just refer to it whenever you want. Once you have the function, that's really no different than finding one in an existing library and then including that one. Either way you're including a function to do the sort comparison for you. In any project, I'm always finding little common functions that I can use in more than one place. – jfriend00 May 27 '14 at 07:14