5

I need to sort an array of objects providing a dictionary with keys and their order. Just like this:

var data = [
    { "name": "a", "age": 1, "money": 4 },
    { "name": "f", "age": 4, "money": 1 },
    { "name": "c", "age": 2, "money": 3 },
    { "name": "a", "age": 0, "money": 1},
    { "name": "f", "age": 4, "money": 3 },
    { "name": "c", "age": 1, "money": 4 },
    { "name": "c", "age": 3, "money": 1 }
];

var data = data.multiSort({
    name: "asc",
    age: "desc",
    money: "desc"
});

console.log(data);
/*
{ "name": "a", "age": 1, "money": 4 },
{ "name": "a", "age": 0, "money": 1},
{ "name": "c", "age": 3, "money": 1 }
{ "name": "c", "age": 2, "money": 3 },
{ "name": "c", "age": 1, "money": 4 },
{ "name": "f", "age": 4, "money": 3 },
{ "name": "f", "age": 4, "money": 1 }
*/

I'm totally stuck and I don't get how to achieve this. Lots people point out this simple piece of code but I do not understand how it is supposed to achieve what I'm trying to do. https://github.com/Teun/thenBy.js

This is the code I have right now. I know I'm pretty far from the solution, but I'd appreciate any help to understand how to get there since I need to improve a lot on javascript.

Array.prototype.multiSort = function(sorters){

    function getNextSorter(sorters, currentSorterKey) {
        var sortersKeys = Object.keys(sorters);
        if(!currentSorterKey)
            currentSorterIndex = 0;
        else
            currentSorterIndex = sortersKeys.indexOf(currentSorterKey) + 1;

        var key = sortersKeys[currentSorterIndex];
        var order = sorters[key];
    }

    function compare(a, b, key, order) {
        var a = a[key];
        var b = b[key];

        //if both numbers compare them as numbers and not as strings
        var numericA = parseFloat(a);
        var numericB = parseFloat(b);
        if(!isNaN(numericA) && !isNaN(numericB)) {
            a = numericA;
            b = numericB;
        }

        //if different compare them with the given order
        if(a != b)
            return (order == "asc") ? a - b : b - a;
        //else compare next key as specified in sorters (if next key is present!)
        else
            //how to recursively call to get the next compare function?
    }

    //how to call sort now?

    return this;
};
4
  • 4
    The trouble with passing an object is that there's no guarantee of order, so you can't be assured that the sort will happen according to the order in which you defined the object properties. Commented Dec 8, 2014 at 18:02
  • The order in the object is the required one. On node the object preserve keys order as they are insert and so they do in chrome. Actually I just need this to work out in chrome v8. Isn't that so? Should I edit the question? Commented Dec 8, 2014 at 18:06
  • Again, the order is not guaranteed. It may appear to give a definite order, but there's nothing guaranteeing that it'll always give that order. Commented Dec 8, 2014 at 18:08
  • then an array with single dictionary is required: [{key:order},{key:order}] Commented Dec 8, 2014 at 18:09

2 Answers 2

3

To make this work reliably, you're going to have to change how you receive the data. An object gives no guarantee of order of enumeration, so it may give the appearance of working for a while, but it can fail at any time.

Instead use some other format. Below I'm going to use strings with a : separator. Like this:

Array.prototype.multiSort = function() {
    // Make an array of arrays:
    // [  ["name" :"asc" ],
    //    ["age"  :"desc"],
    //    ["money":"desc"]   ]
    var sorters = Array.prototype.map.call(arguments, function(s) {
        return s.split(":");
    });

    function compare(a, b) {
        // Iterate over the sorters, and compare using the first non-equal values.
        for (var i = 0; i < sorters.length; i++) {
            var a_val = a[sorters[i][0]];
            var b_val = b[sorters[i][0]];
          
            if (a_val === b_val) {
                continue; // They're equal values, so try the next sorter
            }
            // Swap values if not ascending
            if (sorters[i][1] !== "asc") {
                var temp = a_val;
                a_val = b_val;
                b_val = temp;
            }
            // Use `.localeCompare()` if they're both strings
            if (typeof a_val === "string" && typeof b_val === "string") {
                return a_val.localeCompare(b_val);
            }
            return a_val - b_val;
        }
    }

    return this.sort(compare);
};

var data = [
    { "name": "a", "age": 1, "money": 4 },
    { "name": "f", "age": 4, "money": 1 },
    { "name": "c", "age": 2, "money": 3 },
    { "name": "a", "age": 0, "money": 1 },
    { "name": "f", "age": 4, "money": 3 },
    { "name": "c", "age": 1, "money": 4 },
    { "name": "c", "age": 3, "money": 1 }
];

var data = data.multiSort("name:asc", "age:desc", "money:desc");

document.body.innerHTML = "<pre>" + JSON.stringify(data, null, 4) + "</pre>";

So what this does is it splits each string on the :, so that we get a resulting array with the key to sort and the direction.

Then in the compare function, we just loop over the sorters, grab the value from a and b, and if they're not equal, we go ahead and return the comparison. If they are equal, we need to move on to the next sorter and try with that one.

So there's no real need for an extra function. The for loop is all that's needed.

Sign up to request clarification or add additional context in comments.

8 Comments

How does it works? Array.prototype.multiSort = function() - you don't have a parameter in this function, but you pass it 'data.multiSort("name:asc", "age:desc", "money:desc");' ?
Wow, it works! Why localCompare? Doesn't the simple comparison between string already work in javascript giving the correct order?
@demas arguments is a special var inside a function containing an array with given parameters, in this case an array of strings. pretty clever the way he uses .call on .map giving the explode string as context
@DamianoBarbati: I just updated it to make it a little cleaner, and to only use .localeCompare() if they're both strings. I simply use it because it handles localization in a more sophisticated manner. So we treat anything else as number comparison. The - operator will automatically convert to a number for us if possible.
@sixfingeredman and does the auto casting using the "-" operator work well even with doubles?
|
1

That thenBy script is kind of cool. Very small, but works well. Here is an example using it. Not as extensible as the other example, but might be worth looking at extending to make it more reusable.

The main bit here is that you're passing 3 different sort functions.

// This sorts the name in ASC order
firstBy(function(d1, d2) { 
    if(d1.name < d2.name) return -1; 
    if(d1.name > d2.name) return 1;
    return 0;
})
// this sorts the age in DESC
// note that the > returns -1 but the < returns 1.
// flip these to sort in ASC
.thenBy(function(d1, d2) { 
    if(d1.age > d2.age) return -1;
    if(d1.age < d2.age) return 1;
    return 0;
 })
 // finally sort money in DESC order
 .thenBy(function(d1, d2) { 
    if(d1.money > d2.money) return -1;
    if(d1.money < d2.money) return 1;
    return 0;
 })

Here is a jsFiddle

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.