How to sort array without changing order of same values

Sure thing. The item at index 1 has a score of 120. This is the first time that 120 has been seen so the position doesn’t need to change. The position is normally the index value plus 1. The plus 1 is a human offset because humans count from 1 upwards, whereas computer indexes are from 0 upwards. We add 1 as a humanOffset to the index so that we don’t disturb those pesky humans all too much by the majesty of the zero, and that gives us index + humanOffset which is 2.
The item at index 2 also has a score of 120. This is the second time that 120 has been seen so the position needs to remain the same. The way to do that is to score their position as the index value plus 1, like usual, but subtract from that the number of times that 120 has been seen before. Currently it has been seen once before, so the position is the index value plus 1 (as usual) minus 1 for the number of times that 120 has been seen, giving us index + humanOffset - timesSeen which in this case is 2 + 1 - 1 giving a position of 2.

I’m thinking to give up with this coding challenge and see the solution or still try to find a solution for this coding challenge.

What do you think @Paul_Wilkins, @m_hutley ? What should I do?

The simple answer is to change your comparison function so that it considers both the points and the name at the same time.

function comp (a,b) 
 { var ap=a.points+a.name, bp=b.points+b.name
   if(ap<bp){ return -1 }
   if(ap>bp){ return +1 }
   else { return 0 }
 }  

You then get the required order.

If it were me I would pass the object through several different loops, each one designed to do one simple thing.

I’m not going to tell you whether to give up or not. That’s a you thing.

Does the challenge give you the option to see ‘a’ solution after ‘giving up’? Or are you just going to google around for one?

If you press the solution button and you click on yes, you will not be able to earn coins/rank from this challenge

Well then, its up to you whether or not you give up.

(Though if you do give up, if you need to discuss what they’re doing in their solution, feel free)

1 Like

So I need to pass this function on sort method?

I’m just going to carefully point out to @AllanP that a lexicographical ordering of numeric data is going to fail when there’s an order of magnitude difference.

Good point. If, for instance one of the entries with 120 points was 1200, the sort would fail as you say. This method will only work for the special case where each of the points are in the same range, as in the OP’s original example.

That is correct, but as @m_hutley points out the comparison I have suggested fails if the points strings are different lengths.

I think that enough time has passed for me to work through this problem step by step.

To start with, we have an empty ranking function along with a list of people. https://jsfiddle.net/qgvhw5p4/2/

I’ve added a test for what we want to achieve, where the ranking function is expected to supply an array that matched the expected People array.

describe("ranking people", function() {
    const expectedPeople = [{
        name: "Bob",
        points: 130,
        position: 1,
    }, {
        name: "Kate",
        points: 120,
        position: 2,
    }, {
        name: "Mary",
        points: 120,
        position: 2,
    }, {
        name: "John",
        points: 100,
        position: 4,
    }];
    it("ranks people", function() {
        const rankedPeople = ranking(people);
        expect(rankedPeople).to.eql(expectedPeople);
    });
});

Now clearly, just returning the array is not enough, but it does improve the test from undefined to being an array that doesn’t match.

function ranking(people) {
    return people;
}

I can skip that test for now, while working on other things that need to be done.

The first stage is to sort the array by points, so let’s have a test that helps us to do that.

    it("sorts by points", function () {
        const unsorted = [{points: 5}, {points: 6}];
        const sorted = [{points: 6}, {points: 5}];
        expect(ranking(unsorted)).to.eql(sorted);
    });

Now just sorting the array isn’t enough:

function ranking(people) {
    return people.sort();
}

as we are wanting a reverse sorting of people. So, let’s also reverse the array.

function ranking(people) {
    return people.sort().reverse();
}

That all works, but sorting arrays can get tricky, because the initial array gets changed too. I want the initial array to remain unchanged when the sorting occurs, which means checking that the first array item doesn’t get changed.

    it("leaves the initial array unchanged", function () {
        const unsorted = [{points: 6}, {points: 5}];
        ranking(unsorted);
        expect(unsorted[0].points).to.eql(6);
    });

We can deal with that issue by making a copy of the array. A common way to copy an array is to use the slice method, which returns a separate copy of the array.

function ranking(people) {
    const sorted = people.slice().sort();
    return sorted.reverse();
}

There is a more advanced way of cloning an array that is more expressive, and that is to use the spread operator which looks like three dots. ...

function ranking(people) {
    const sorted = [...people].sort();
    return sorted.reverse();
}

Something else that can cause trouble with arrays is lexicographical ordering as was mentioned in post #30 so let’s sort 25, 3, and 400.

    it("sorts in numeric order", function () {
        const unsorted = [{points: 25}, {points: 3}, {points: 400}];
        const sorted = [{points: 400}, {points: 25}, {points: 3}];
        expect(ranking(unsorted)).to.eql(sorted);
    });

We can deal with that by using a sorting function that returns one number minus the other number.

function ranking(people) {
    const sorted = [...people].sort(function (a, b) {
        return a.points - b.points;
    });
    return sorted.reverse();
}

That passes all of the tests. Now that we have this custom sorting function, we can have it sort in reverse order without needing the separate reverse statement too.

function ranking(people) {
    const sorted = [...people].sort(function (a, b) {
        return b.points - a.points;
    });
    return sorted;
}

We now have an array that is being properly sorted in reverse order. https://jsfiddle.net/qgvhw5p4/5/

Next up is to add a position to each array item. Let’s start off nice and simple by expecting a position value:

    it("adds a position to each array item", function () {
        const unsorted = [{points: 5}, {points: 6}];
        const sorted = [{points: 6}, {points: 5}];
        const ranked = ranking(unsorted);
        expect(ranked[0].position).to.equal(1);
        expect(ranked[1].position).to.equal(2);
    });

We can use the map method to add a position value to each item:

function ranking(people) {
    const sorted = [...people].sort(function (a, b) {
        return b.points - a.points;
    });
    const ranked = sorted.map(function (item, index) {
        item.position = index + 1;
        return item;
    });
    return ranked;
}

While that works with the test, it breaks other tests. Those other tests that break are checking that one array precisely equals another one. Instead of making such a deep comparison, we’ll remove the second array from the test, and just check each array item instead.

    it("sorts by points", function () {
        const unsorted = [{points: 5}, {points: 6}];
        const ranked = ranking(unsorted);
        expect(ranked[0].points).to.equal(6);
        expect(ranked[1].points).to.equal(5);
    });

Those prior tests are now all working again. https://jsfiddle.net/qgvhw5p4/6/

Now that the tests are all passing we can refactor our code to make improvement.
Instead of updating the item with the position and then returning it, I want to just return an updated item. That can be done using the Object.assign method

    const ranked = sorted.map(function (item, index) {
        return Object.assign(item, {position: index + 1});
    });

https://jsfiddle.net/qgvhw5p4/7/

Before going further with position, we need to deal with scores that are the same. When that occurs we want them to be sorted by the persons name.

    it("sorts people with the same points", function () {
        const unsorted = [{name: "Mary", points: 5}, {name: "Mark", points: 5}];
        const ranked = ranking(unsorted);
        expect(ranked[0].name).to.equal("Mark");
        expect(ranked[1].name).to.equal("Mary");
    });

That test currently fails, but by adding an if statement to the sorting function:

    const sorted = [...people].sort(function (a, b) {
        if (b.points === a.points) {
            return (a.name < b.name ? -1 : 1);
        }
        return b.points - a.points;
    });

everything now passes. https://jsfiddle.net/qgvhw5p4/8/

There is only one last detail to take care of and that is ensuring that the position remains the same when the score is the same.

    it("keeps the position the same for people with the same points", function () {
        const unsorted = [{name: "Mary", points: 5}, {name: "Mark", points: 5}];
        const ranked = ranking(unsorted);
        expect(ranked[0].position).to.equal(ranked[1].position);
    });

One way to deal with this is to check if the points are the same. If they are, we can just copy the position from that previous item. And clearly, we can’t do that on the zero’th item either.

    const ranked = sorted.map(function (item, index, arr) {
        if (index > 0 && arr[index - 1].points === item.points) {
            const position = arr[index - 1].position;
            return Object.assign(item, {position});
        }
        return Object.assign(item, {position: index + 1});
    });

And all of the tests now pass. Let’s now remove skip from that very first test and see if it passes too:

    it("ranks people", function() {
        const rankedPeople = ranking(people);
        expect(rankedPeople).to.eql(expectedPeople);
    });

Yes, it all passes. We now have code that properly achieves all that is expected. https://jsfiddle.net/qgvhw5p4/9/

1 Like

Psst, Paul:

Your test fails if 3 or more people have the same score.
EDIT: Nope. I’m just dumb at 5 AM and screwed up my assertion because I didnt adjust the name list when i added a third person. Derp.

1 Like

Ooh! More tests! I’m surprised though because the code passed at codeWars. Still, the opportunity to improve is not sneered at.

Let’s find a test case that demonstrates the problem.

    it("keeps the same position for several people with the same points", function () {
        const unsorted = [
            {name: "Suzy", points: 4},
            {name: "Mary", points: 5},
            {name: "Jean", points: 5},
            {name: "Mark", points: 5},
            {name: "John", points: 6}
        ];
        const ranked = ranking(unsorted);
        expect(ranked[0].name).to.equal("John");
        expect(ranked[0].position).to.equal(1);
        expect(ranked[1].name).to.equal("Jean");
        expect(ranked[1].position).to.equal(2);
        expect(ranked[2].name).to.equal("Mark");
        expect(ranked[2].position).to.equal(2);
        expect(ranked[3].name).to.equal("Mark");
        expect(ranked[3].position).to.equal(2);
        expect(ranked[4].name).to.equal("Suzy");
        expect(ranked[4].position).to.equal(5);
    });

Oof, that’s busy. Let’s simplify it by separating out the names and the positions.

    it("keeps the same position for several people with the same points", function () {
        const unsorted = [
            {name: "Suzy", points: 4},
            {name: "Mary", points: 5},
            {name: "Jean", points: 5},
            {name: "Mark", points: 5},
            {name: "John", points: 6}
        ];
        const ranked = ranking(unsorted);
        const names = ranked.map((person) => person.name);
        const positions = ranked.map((person) => person.position);
        expect(names).to.eql(["John", "Jean", "Mark", "Mary", "Suzy"]);
        expect(positions).to.eql([1, 2, 2, 2, 5]);
    });

That all seems to work.

Whoops, okay good. That’s a nice benefits of having deeper tests - any potential issues can also be clarified and taken care of.

So here’s my version. More compact, less readable, as always, than Paul’s excellent explanations:

function ranking(people) {
  let sorted = people.map((x) => x.points).sort((x,y) => parseInt(x) - parseInt(y)).reverse();
  return people.map((x) => { x.position = sorted.indexOf(x.points)+1; return x;}).sort((x,y) => (x.position != y.position) ? x.position - y.position : (x.name > y.name) ? 1 : -1)
}

Does a little less comparing against neighbors, and relies instead on the idea that indexOf will always stop when it finds the first value in the array.

1 Like

I try to act as if a great big hulking gorilla will be working beside me in 6-months, after I’ve forgotten the details of how it works.

The Hulk works too :slight_smile:

2 Likes

See, and usually for me that hulking gorilla is… me. Sitting there going “WTF were you thinking…” to myself about my own code.

2 Likes

Here’s the secret, well one of many.

“Simple” is not a baby beginner thing. After having gone through a lot of complex ways to do things, simple ends up being a higher state of enlightenment.

When doing complex work and you then need to debug it, debugging is several order of magnitude more difficult than coding. When your coding is at the extent of your abilities, it is not possible to debug that code. That’s why keeping things simple helps to make debugging when it’s needed, easier to achieve too.

2 Likes

Absolutely true. Though to be fair, usually the hulking gorilla is sitting there saying “WTF were you thinking” because the follow up is “There’s SO much easier/better ways to do that…”

I agree. If we change the comparison function slightly we get all the right answers.

function comp (a,b)
   { if(a.points<b.points){ return -1 }
   if(a.points>b.points){ return +1 }
   else 
   { if(a.name<b.name){ return -1 }
   if(a.name>b.name){ return +1 } 
   else return 0 
   }
  }  

This works for all values of points and all duplications of names.