9

I'm trying to sort an array of values that can be a mixture of numeric or string values (e.g. [10,"20",null,"1","bar","-2",-3,null,5,"foo"]). How can I sort this array such that

  • null values are always placed last (regardless of sorting order, see jsFiddle)
  • negative numbers are sorted correctly (i.e. they are less than positive numbers and sort correctly amongst themselves)

? I made a jsFiddle with detailed numeric and string examples (using localeCompare and the numeric option), but will paste the numeric version of my sorting algorithm below as a starting point.

// Sorting order
var order = "asc"; // Try switching between "asc" and "dsc"

// Dummy arrays
var numericArr = [10,20,null,1,-2,-3,null,5];

// Sort arrays
$(".output1").append(numericArr.toString());
numericArr.sort(sortByDataNumeric);
$(".output2").append(numericArr.toString());

// Numeric sorting function
function sortByDataNumeric(a, b, _order) {

    // Replace internal parameters if not used
    if (_order == null) _order = order;

    // If values are null, place them at the end
    var dflt = (_order == "asc" ? Number.MAX_VALUE : -Number.MAX_VALUE);

    // Numeric values
    var aVal = (a == null ? dflt : a);
    var bVal = (b == null ? dflt : b);
    return _order == "asc" ? (aVal - bVal) : (bVal - aVal);
}

The problem with my string sorting algorithm (see jsFiddle) is that I can't find a way to always place null values last and negative values aren't correctly sorted within themselves (e.g. -3 should be less than -2)

Edit

To answer the comments, I expect [10,"20",null,"1","bar","-2",-3,null,5,"foo"] to sort to [-3,"-2","1",5,10,"20","bar","foo",null,null]

lebolo
  • 2,120
  • 4
  • 29
  • 44
  • 1
    What should happen to `"bar"`? It's neither a number, a numeric string nor `null`. – Bergi Sep 30 '13 at 18:53
  • 1
    and should "20" (string) sort before or after 20 (integer)? Or after all numeric values? – andi Sep 30 '13 at 18:55
  • Are you referring to the alpha sort, rather than the code you show? Alpha order isn't the same as numeric order, and you shouldn't be surprised at the order shown. – Dave Newton Sep 30 '13 at 18:56
  • 1
    How do you want to sort the strings? Relevant example: ["-1", 3, "10", "foo"] – Tibos Sep 30 '13 at 19:01
  • @Bergi Added the expected answer, thanks. – lebolo Sep 30 '13 at 19:23
  • Good question @andi, I would actually prefer they get compared as equal... – lebolo Sep 30 '13 at 19:24
  • @DaveNewton The jsFiddle shows what order I'm talking about (you can toggle between asc/dsc to see what I mean) – lebolo Sep 30 '13 at 19:25
  • ok, I'm trying it now. – andi Sep 30 '13 at 19:38
  • @lebolo And I'm telling you that when you're sorting strings that "-2" and "-3" are being sorted correctly; you need to convert numeric strings to numbers if you want them in numerical, not lexicographical, order. – Dave Newton Sep 30 '13 at 19:49
  • @DaveNewton Oh, I see what you mean. Yes I understand and you're right. But your comment doesn't help me get from `[10,"20",null,"1","bar","-2",-3,null,5,"foo"]` to `[-3,"-2","1",5,10,"20","bar","foo",null,null]`, understand? – lebolo Sep 30 '13 at 20:15
  • @lebolo It should, because I told you what you needed to do to handle numeric strings properly. – Dave Newton Sep 30 '13 at 20:26

4 Answers4

11

You should first check to see if either value is null and return the opposite value.


On a side note:

For your default _order value, you should check if the parameter is undefined instead of comparing its value to null. If you try to compare something that is undefined directly you will get a reference error:

(undefinedVar == null) // ReferenceError: undefinedVar is not defined

Instead, you should check if the variable is undefined:

(typeof undefinedVar == "undefined") // true

Also, it's probably a better idea to wrap your compare function in a closure instead of relying on a global order variable.

Sometime like:

[].sort(function(a, b){ return sort(a, b, order)})

This way you can sort at a per-instance level.


http://jsfiddle.net/gxFGN/10/

JavaScript

function sort(a, b, asc) {
    var result;

    /* Default ascending order */
    if (typeof asc == "undefined") asc = true;

    if (a === null) return 1;
    if (b === null) return -1;
    if (a === null && b === null) return 0;

    result = a - b;

    if (isNaN(result)) {
        return (asc) ? a.toString().localeCompare(b) : b.toString().localeCompare(a);
    }
    else {
        return (asc) ? result : -result;
    }
}
Community
  • 1
  • 1
thgaskell
  • 12,772
  • 5
  • 32
  • 38
  • 2
    This does not work. You didn't return `0` if both operands are `null`, and even worse it *throws an exception `a.localeCompare is not a function`* when comparing `10` with `"bar"` - the fiddle is broken (happens with Opera sort algorithm) – Bergi Oct 01 '13 at 13:40
  • 1
    I accepted this answer, it's the most concise and cleanest IMO. However, I modified the code a bit to be more specific about null checks: `if (a === null && b !== null) return 1; if (a !== null && b === null) return -1; var result = a - b;`. This makes it a bit more consistent for spec purposes (almost useless from a practical point of view). I also force a `toString` before calling the `localCompare` function: `a.toString().localeCompare(b)`. – lebolo Oct 01 '13 at 14:46
  • Thanks @Bergi. I've updated the the question with both your feedback. I incorrectly assumed `result === NaN` only when both the strings could not be coerced to numeric values. – thgaskell Oct 01 '13 at 15:11
2
function sortByDataString(a, b) {
    if (a === null) {
        return 1;
    }
    if (b === null) {
        return -1;
    }
    if (isNumber(a) && isNumber(b)) {
        if (parseInt(a,10) === parseInt(b,10)) {
            return 0;
        }
        return parseInt(a,10) > parseInt(b,10) ? 1 : -1;
    }
    if (isNumber(a)) {
        return -1;
    }
    if (isNumber(b)) {
        return 1;
    }
    if (a === b) {
        return 0;
    }
    return a > b ? 1 : -1;
}

fiddle here: http://jsfiddle.net/gxFGN/6/

I left out the order parameter, but you could always reverse the array at the end if needed.

andi
  • 6,442
  • 1
  • 18
  • 43
  • Running your jsFiddle gives me a result of `-2,1,10,20,bar,-3,5,foo,null,null` but I want `-3,-2,1,5,10,20,bar,foo,null,null` – lebolo Sep 30 '13 at 20:05
  • weird... I get the result "After sort: -3,-2,1,5,10,20,bar,foo,,". Is it browser dependent? I'm going to try all my other browsers now. – andi Sep 30 '13 at 20:08
  • whoa - I just tried 3 different browsers and got 3 different results. I guess this ISN'T simple. =) – andi Sep 30 '13 at 20:09
  • You are never returing 0 even when the items are equal. Sorting algorithms expect a proper compare function, otherwise they get unpredictable (and 3 different unpredictable results from 3 different sort implementations, i.e. browsers) – Bergi Sep 30 '13 at 20:33
  • @Bergi - you're right, it would be better to return 0 if the strings were equal, but that wasn't the reason for the crazy differences between browsers - I saw the same issue either way. I'll add it in to this working function, though. – andi Sep 30 '13 at 20:52
  • @andi: What did you change? [It would work correctly with 0](http://jsfiddle.net/gxFGN/7/). – Bergi Sep 30 '13 at 20:57
  • @Bergi - similar to yours, but also added in the return 0 in the `if (isNumber(a) && isNumber(b))` case. – andi Sep 30 '13 at 20:59
  • You are absolute genius. – Biswas Khayargoli Jun 27 '16 at 07:51
1

Use this:

function typeOrder(x) {
    if (x == null)
        return 2;
    if (isNaN(+x))
        return 1;
    return 0;
}
function sortNumber(a, b) {
    a = parseInt(a, 10); b = parseInt(b, 10);
    if (isNaN(a) || isNaN(b))
        return 0;
    return a - b;
}
function sortString(a, b) {
    if (typeof a != "string" || typeof b != "string")
       return 0;
    return +(a > b) || -(b > a);
}

order = order == "dsc" ? -1 : 1;
numericArr.sort(function(a, b) {
    return order * ( typeOrder(a)-typeOrder(b)
                     || sortNumber(a, b)
                     || sortString(a, b)
                   );
});

(updated fiddle)

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

I'm pretty sure that your problem is a red herring... the abstract function that you past into sort doesn't get a third parameter (in your case _order). So in your situation that's always going to be undefined.

Please reconsider your code with that in mind and see what you get.

The array you specify is entirely Numeric so your sort should work correctly, though as other commenters have suggested, if your array ever winds up with string values (i.e. "10", "-7" etc) you'll want to parseInt and test for isNaN before doing your comparison.

Yevgeny Simkin
  • 27,946
  • 39
  • 137
  • 236
  • I know, that's why I do `if (_order == null) _order = order` since `order` is in scope. Try changing the `order` variable between 'asc'/'dsc' in the jsFiddle to see that it works. The jsFiddle also has the string array example. It's the combination of numeric and string and null that becomes difficult. – lebolo Sep 30 '13 at 19:26
  • 1
    but it's not `null` it's `undefined` which is not the same thing. – Yevgeny Simkin Sep 30 '13 at 21:29