Search code examples
javascriptarraysjavascript-objects

Custom sorting array of objects


I have an array of objects like so for example:

[ {
    id: '-1'
},
{
    id: '10'
},
{
    id: '1234'
},
{
    id: '1235'
},
{
    id: '-1'
} ]

I would like to sort this array so that it is ordered by the ids ascending (smallest to largest), however I would like objects that have the id of '-1' to be sent to the back of the array. So I tried this:

const arr = [ {	id: '-1' }, { id: '10'}, { id: '1234' }, { id: '1235' }, { id: '-1' } ]
arr.sort((a, b) => {
	if (parseInt(a.id) === -1 || parseInt(b.id) === -1) return 1;
	if (parseInt(a.id) - parseInt(b.id)) return -1;
});
console.log(arr);

As you can see from the snippet above it sorts them successfully ascending however it doesn't successfully move ids with '-1' to the back of the array.

Why does this happen? What am I doing wrong?


Solution

  • How sorting works

    First, let's take a look at what .sort expects you to feed it.

    Given paramaters (a, b) -- we should return

    • < 0 if a < b
    • > 0 if a > b

    How you did the sorting

    Let's take a look at your first line

    if (parseInt(a.id) === -1 || parseInt(b.id) === -1) return 1;
    

    -1 means "a is smaller". But that might not be true. It might be that a (not b) is -1, and is bigger, yet we're always telling JavaScript to send a to the back when that's what we should be doing to b.

    Instead, we want to return -1 when b is -1, but +1 when a is -1. It doesn't really matter what we return if they're both -1.



    In JavaScript, any number except for 0 is truthful. Let's take a look at your next line.

    if (parseInt(a.id) - parseInt(b.id)) return -1;
    

    If a - b is 0, we don't return anything in this callback. If it isn't 0, we always say that a < b.

    Notice that never do we say that b < a -- so if such an event were to occur, we couldn't handle it and would sort it incorrectly.

    How to sort correctly

    Fetch IDs

    const aID = parseInt(a.id)
    const bID = parseInt(b.id)
    

    Is a or b -1?

    if(aID === -1) return +1;
    if(bID === -1) return -1;
    

    Which is bigger, a or b

    If you assume that a and b are not -1, then we can simply subtract a - b. Why? If a is bigger than b, then the subtraction will create a positive number to say b < a. If a is less than b, then the subtraction will create a negative number, to say a < b.

    return aID - bID
    

    All together

    const arr = [ {	id: '-1' }, { id: '10'}, { id: '1234' }, { id: '1235' }, { id: '-1' } ]
    arr.sort((a, b) => {
    	const [aID, bID] = [parseInt(a.id), parseInt(b.id)]
      if(aID === -1) return +1
      if(bID === -1) return -1
      return aID - bID
    });
    console.log(arr);

    Golfing

    It might be helpful to make things shorter. Another answer, by @Nina Scholz, helpfully showed a much shorter version. I thought it might be useful to explain why this works.

    return (a.id === '-1') - (b.id === '-1') || a.id - b.id
    

    What is x || y

    x || y means:

    • if x is truthful, return x.
    • if x isn't truthful, return y.

    What is (aID === -1)

    This means true if aID is -1, and false otherwise

    What is (aID === -1) - (bID === -1)

    How can you subtract true and false? true will be interpreted as 1, and false as 0.

    aID = -1, bID = not

    This value will be 1 - 0, or +1

    aID = not, bID = -1

    This value will be 0 - 1, or -1

    aID = -1, bID = -1

    Remember, it doesn't matter what we return if the two values are the same

    aID = not, bID = not

    0 - 0. This is 0. This is not a truthful value. So we go into the || bit. Which, in that answer, has the second bit be aID - bID, as described above. It's very clever and very short, though might not be as readable.