8

I have two arrays of objects like below:

items = [{"id":"5","tobuy":"1","name":"pop"},
         {"id":"6","tobuy":"1","name":"fish"},
         {"id":"7","tobuy":"0","name":"soda"}]
pkgs =  [{"item_id":"5","store":"Market","aisle":"3"},
         {"item_id":"6","store":"Market","aisle":"2"},
         {"item_id":"6","store":"Dept","aisle":"8"},
         {"item_id":"7","store":"Market","aisle":"4"}]

I'm trying to sort the items array, but I want to leverage the data in the pkgs array.
The "item_id" field in the pkgs array corresponds to the "id" field in the items array.
For example, I want to sort:

  • first by "tobuy" in descending order
  • then by "store"
  • then by "aisle"
  • then by "name"

While item_id and id correspond between the two arrays, there is not a 1 to 1 relationship. There could be 0 or more pkgs that correspond to any given item.

(If I had a database, I would just join the tables, but in JavaScript I just have the two related arrays).

I'm not sure how to build the comparator function and pass in the second array.

Thanks for any help.

Simpler
  • 1,317
  • 2
  • 16
  • 31
  • you can use underscoreJS. – Vural Oct 09 '13 at 01:41
  • @Vural, I looked through the [underscoreJS](http://underscorejs.org/) functions and I'm not sure how it helps. Can you provide any additional direction? Thanks – Simpler Oct 09 '13 at 01:51
  • 1
    Check here: http://stackoverflow.com/questions/1129216/sorting-objects-in-an-array-by-a-field-value-in-javascript , find the dynamicSort – Vural Oct 09 '13 at 01:54
  • @VuralAcar, I've reviewed dynamicSort on this link, but it only sorts relative to the multiple fields in the single array. I need to use the data from the second array to sort the first array. – Simpler Oct 09 '13 at 02:00
  • So are you looking to join the two array together with matching `id` and `item_id`? – Qantas 94 Heavy Oct 09 '13 at 02:06
  • @Qantas94Heavy, I don't really want to join the two together in the end. I only need the data from pkgs during the sort. In the end I only want the data in the original items array (just sorted). Do I have to join the two arrays and then strip out the data from the pkgs array after doing the sort? – Simpler Oct 09 '13 at 02:11
  • lookup Schwartzian Transform in Perl - basically, it is a map to augment the one array with the to-be-sorted field from the other array, followed by a sort, followed by a map to strip the augmented value from the sorted result. – ChuckCottrill Oct 09 '13 at 02:57
  • @ChuckCottrill Exactly what the first part of my answer does ;) – plalx Oct 09 '13 at 02:59

3 Answers3

4

Perhaps something like this?

items = items.map(function (item, index) {
    return {
        item: item,
        pkg: pkgs[index] //I assumed associated pkgs were at the same index
    };
}).sort(function (a, b) {
   var pkgA = a.pkg, pkgB = b.pkg, r;

   r = +b.item.tobuy - +a.item.tobuy;

   if (r !== 0) return r;

   r = pkgA.store < pkgB.store? -1 : (pkgA.store === pkgB.store? 0 : 1);

   if (r !== 0) return r;

   r = +pkgA.aisle - +pkgB.aisle;

   if (r !== 0) return r;

   return pkgA.name < pkgB.name? -1 : (pkgA.name === pkgB.name? 0 : 1);

}).map(function (item) {
    return item.item;
});

Instead of merging the data, you could also create a lookup map that allows to quickly retrieve the associated package directly from the sort function.

E.g.

var pkgsMap = pkgs.reduce(function (res, pkg) {
    res[pkg.item_id] = pkg;
    return res;
}, {});

Then in the sort function you can do:

var pkgA = pkgsMap[a.id], pkgB = pkgsMap[b.id];

EDIT:

There is actually another field in the pkgs array called "ppu" which is the price per unit. The lowest ppu is the one that would be used.

You can just build your package map using the following and then use the map in the sort function to retrieve the associated package like described above and implement the sort algorithm.

var pkgsMap = pkgs.sort(function (a, b) {
    //not sure what ppu is so I sort it as a string
    return a.ppu < b.ppu? -1 : Number(a.ppu > b.ppu);
}).reduce(function (res, pkg) {
    if (!(pkg.item_id in res)) res[pkg.item_id] = pkg;
    return res;
}, {});
plalx
  • 42,889
  • 6
  • 74
  • 90
  • +1 for the Schwartzian Transform (though it is a bit longer than the ST's I'm used to seeing ;-) – ChuckCottrill Oct 09 '13 at 03:02
  • @plalx, I should have mentioned earlier, the indexes between the two arrays are not the same. There may be 0 or more pkgs that correspond to a single item. – Simpler Oct 09 '13 at 03:04
  • @Simpler How do you want to deal with cases where multiple packages are associated with a single item? – plalx Oct 09 '13 at 03:07
  • @Simpler As for the index not being the same, you can simply use the second approach that I describe to get the associated package. – plalx Oct 09 '13 at 03:21
  • @plalx, There is actually another field in the pkgs array called "ppu" which is the price per unit. The lowest ppu is the one that would be used. (In trying to simplify the problem in the original post, I left out some useful information by mistake.) – Simpler Oct 09 '13 at 03:22
  • @Simpler, I updated my answer. You should have everything you need to solve you problem now. If you have any questions, let me know. – plalx Oct 09 '13 at 03:40
1

Make a function that generates a comparator, this looks unwieldy but means you can generate any sort order desired

function generateComparator(dict, index, order) {
    return function (a, b) {
        var i, key, direction,
            ai = a[index], av,
            bi = b[index], bv;
        for (i = 0; i < order.length; ++i) {
            key = order[i].key;
            direction = +!!order[i].reverse || -1;
            if (dict[ai].hasOwnProperty(key)) // if in dict, lookup
                av = dict[ai][key];
            else                              // else look up in item
                av = a[key];
            if (dict[bi].hasOwnProperty(key))
                bv = dict[ai][key];
            else
                bv = b[key];
            // console.log(i, key, av, bv, direction); // debug
            if (av === bv)
                continue;
            if (av < bv)
                return direction;
            return -direction;
        }
        return 0;
    };
}

Convert your Arrays into a dictionary

var dict = (function (arr, index) {
    var o = {}, i;
    for (i = 0; i < arr.length; ++i) {
        o[arr[i][index]] = arr[i];
    }
    return o;
}(pkgs, 'item_id'));

Define your sort choices

var order = [
    {key: 'tobuy', reverse: 1},
    {key: 'store'},
    {key: 'aisle'},
    {key: 'name'}
];

Generate comparator with dictionary

var comparator = generateComparator(dict, 'id', order);

Then sort your first Array

items.sort(comparator);
/* [
    {"id": "6", "tobuy": "1", "name": "fish"},
    {"id": "5", "tobuy": "1", "name": "pop"},
    {"id": "7", "tobuy": "0", "name": "soda"}
] */
Paul S.
  • 64,864
  • 9
  • 122
  • 138
  • +1, I believe that it would be more reusable to let the client code merge the properties and have a `generateComparator` function that doesn't rely on an external source of data. Also, you may want to account for `'9' > '11'` which evaluates to `true`. – plalx Oct 09 '13 at 02:54
  • @plalx OP uses _String_ for them so I didn't want to make guesses. The external `dict` for `generateComparator` was to make it as generic as possible, of course if it's not suitable, changes could always be made, but I hope I got the "difficult" concepts down so such modification requires minimal effort. – Paul S. Oct 09 '13 at 03:08
1

Let's consider how you would do this in SQL:

SELECT * FROM items INNER JOIN pkgs ON items.id = pkgs.item_id
ORDER BY tobuy DESC, store, aisle, name

The following answer demonstrates how to implement an inner join and an equijoin in JavaScript:

function equijoin(primary, foreign, primaryKey, foreignKey, select) {
    var m = primary.length, n = foreign.length, index = [], c = [];

    for (var i = 0; i < m; i++) {     // loop through m items
        var row = primary[i];
        index[row[primaryKey]] = row; // create an index for primary table
    }

    for (var j = 0; j < n; j++) {     // loop through n items
        var y = foreign[j];
        var x = index[y[foreignKey]]; // get corresponding row from primary
        c.push(select(x, y));         // select only the columns you need
    }

    return c;
}

Now you can use equijoin to join items and pkgs as follows:

equijoin(items, pkgs, "id", "item_id", function (item, pkg) {
    return {
        id: +item.id,
        tobuy: +item.tobuy,
        store: pkg.store,
        aisle: +pkg.aisle,
        name: item.name
    };
});

Note that I'm coercing item.id, item.tobuy and pkg.aisle to numbers by applying the unary + operator to them.

Now that we joined the two tables we need to sort them. To sort the table we use the built-in array sort method:

.sort(function (a, b) {
    // ORDER BY tobuy DESC

    var aTobuy = a.tobuy, bTobuy = b.tobuy;

    if (aTobuy < bTobuy) return 1;
    if (aTobuy > bTobuy) return -1;

    // ORDER BY store

    var aStore = a.store, bStore = b.store;

    if (aStore < bStore) return -1;
    if (aStore > bStore) return 1;

    // ORDER BY aisle

    var aAisle = a.aisle, bAisle = b.aisle;

    if (aAisle < bAisle) return -1;
    if (aAisle > bAisle) return 1;

    // ORDER BY name

    var aName = a.name, bName = b.name;

    if (aName < bName) return -1;
    if (aName > bName) return 1;

    // keep them unchanged

    return a.id - b.id;
});

The sort method is unstable (i.e. it might not preserve the ordering of items with equal sort value in the input list). Hence to workaround this limitation we return a.id - b.id as the last statement.

Also notice that we're comparing all the values (whether strings or numbers) using < and >. Strings are compared lexicographically while numbers are compared numerically.

Put together the code is as follows:

var table = equijoin(items, pkgs, "id", "item_id", function (item, pkg) {
    return {
        id: +item.id,
        tobuy: +item.tobuy,
        store: pkg.store,
        aisle: +pkg.aisle,
        name: item.name
    };
}).sort(function (a, b) {
    var aTobuy = a.tobuy, bTobuy = b.tobuy;

    if (aTobuy < bTobuy) return 1;
    if (aTobuy > bTobuy) return -1;

    var aStore = a.store, bStore = b.store;

    if (aStore < bStore) return -1;
    if (aStore > bStore) return 1;

    var aAisle = a.aisle, bAisle = b.aisle;

    if (aAisle < bAisle) return -1;
    if (aAisle > bAisle) return 1;

    var aName = a.name, bName = b.name;

    if (aName < bName) return -1;
    if (aName > bName) return 1;

    return a.id - b.id;
});

Not as concise as SQL is it? Anyway, see the demo for yourself: http://jsfiddle.net/7ZG96/


Edit: If you want only want the id, tobuy and name columns then you can extract it from the sorted table using map as follows:

table.map(function (item) {
    return {
        id: item.id,
        tobuy: item.tobuy,
        name: item.name
    };
});

This corresponds to the following SQL query:

SELECT id, tobuy, name FROM (SELECT * FROM items INNER JOIN pkgs
ON items.id = pkgs.item_id ORDER BY tobuy DESC, store, aisle, name)

See the updated demo: http://jsfiddle.net/7ZG96/1/

Community
  • 1
  • 1
Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • 2
    +1, it would be sad not to be rewarded for taking the time to write such a quality answer ;) – plalx Oct 09 '13 at 03:50